diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..136aa8b2 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-05-18 - Fix Command Injection in API execution +**Vulnerability:** The `src/pages/api/docker-logs.ts` API route had a CRITICAL command injection vulnerability where unsanitized user inputs (`containerId` and `tail`) were directly interpolated into a string executed by `execSync`. +**Learning:** Shell interpreters like `execSync` evaluate meta-characters natively, permitting command breakouts. +**Prevention:** Always use safe primitives like `execFile` or `execFileAsync` where parameters are passed explicitly as an array. Furthermore, thoroughly validate dynamic inputs using regex (e.g., `/^[a-zA-Z0-9_.-]+$/`) to reject special characters, prohibit starting hyphens (`-`) to mitigate flag injection, and use the `--` double-dash operator to demarcate options from positional arguments. diff --git a/src/pages/api/docker-logs.ts b/src/pages/api/docker-logs.ts index cb72e09e..48522f6c 100644 --- a/src/pages/api/docker-logs.ts +++ b/src/pages/api/docker-logs.ts @@ -1,5 +1,8 @@ import type { APIRoute } from 'astro'; -import { execSync } from 'child_process'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; + +const execFileAsync = promisify(execFile); export const GET: APIRoute = async ({ url }) => { try { @@ -14,21 +17,25 @@ export const GET: APIRoute = async ({ url }) => { const containerId = url.searchParams.get('id'); const tail = url.searchParams.get('tail') || '100'; - if (!containerId) { - return new Response(JSON.stringify({ error: "ID du conteneur manquant" }), { + if (!containerId || !/^[a-zA-Z0-9_.-]+$/.test(containerId) || containerId.startsWith('-')) { + return new Response(JSON.stringify({ error: "ID du conteneur invalide ou manquant" }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + if (!/^\d+$/.test(tail) && tail !== 'all') { + return new Response(JSON.stringify({ error: "Paramètre 'tail' invalide" }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } - // Commande Docker pour récupérer les logs - const command = `docker logs --tail ${tail} ${containerId}`; - let logs = []; + let logs: string[] = []; try { - const output = execSync(command, { stdio: ['pipe', 'pipe', 'pipe'] }).toString(); - logs = output.trim().split('\n'); + const { stdout, stderr } = await execFileAsync('docker', ['logs', '--tail', tail, '--', containerId]); + logs = (stdout || stderr).trim().split('\n'); } catch (err: any) { - // Certains logs sortent sur stderr, checkons stderr si stdout est vide ou si erreur if (err.stderr) { logs = err.stderr.toString().trim().split('\n'); } else { @@ -41,7 +48,8 @@ export const GET: APIRoute = async ({ url }) => { headers: { 'Content-Type': 'application/json' } }); } catch (error: any) { - return new Response(JSON.stringify({ error: "Logs indisponibles: " + error.message }), { + // Sanitize error message to prevent leaking sensitive information + return new Response(JSON.stringify({ error: "Logs indisponibles: Une erreur inattendue s'est produite." }), { status: 500, headers: { 'Content-Type': 'application/json' } });