From a4bb5ea056eeba1e35279ebcc8c64d8074143da7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 17:39:56 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20command=20injection=20in=20docker=20logs=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced synchronous `execSync` with `execFileAsync` in `src/pages/api/docker-logs.ts` to prevent command injection and event loop blocking. Used argument arrays, validated `containerId` to prevent flag injection, sanitized error output, and documented findings in `.jules/sentinel.md`. Co-authored-by: bobdivx <6737167+bobdivx@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ src/pages/api/docker-logs.ts | 43 +++++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..0b8a1812 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-05-15 - Command Injection in Docker Exec Sync +**Vulnerability:** Found a critical command injection vulnerability in `src/pages/api/docker-logs.ts` where unvalidated user query parameters (`id`, `tail`) were directly concatenated into a shell string and executed via `child_process.execSync`. +**Learning:** Node.js `execSync` combined with string interpolation of user parameters inherently leads to shell command injection and simultaneously blocks the entire single-threaded event loop, magnifying the risk with a Denial-of-Service vector. +**Prevention:** Always use `execFile` or `execFileAsync` with an explicit arguments array instead of executing raw shell strings. Additionally, strictly validate and sanitize any parameters (e.g., verifying parameters do not start with a hyphen to prevent flag injection, coercing types like strings to integers where applicable). diff --git a/src/pages/api/docker-logs.ts b/src/pages/api/docker-logs.ts index cb72e09e..e1ff6227 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 { @@ -12,27 +15,44 @@ export const GET: APIRoute = async ({ url }) => { } const containerId = url.searchParams.get('id'); - const tail = url.searchParams.get('tail') || '100'; + const tailStr = url.searchParams.get('tail') || '100'; - if (!containerId) { + if (!containerId || typeof containerId !== 'string') { return new Response(JSON.stringify({ error: "ID du conteneur manquant" }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } - // Commande Docker pour récupérer les logs - const command = `docker logs --tail ${tail} ${containerId}`; - let logs = []; + // Prevent argument injection + if (containerId.startsWith('-')) { + return new Response(JSON.stringify({ error: "ID du conteneur invalide" }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + const tail = parseInt(tailStr, 10); + if (isNaN(tail) || tail < 0) { + return new Response(JSON.stringify({ error: "Paramètre tail invalide" }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + 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.toString(), containerId], { timeout: 10000 }); + const output = stdout.trim() || stderr.trim(); + logs = output ? output.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'); + const stderrStr = err.stderr.toString().trim(); + logs = stderrStr ? stderrStr.split('\n') : []; } else { - throw err; + // Sanitize error to prevent leaking secrets/internal state + throw new Error("Erreur lors de la récupération des logs Docker"); } } @@ -41,7 +61,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 }), { + // Return sanitized error + return new Response(JSON.stringify({ error: "Logs indisponibles: " + (error.message || "Erreur interne") }), { status: 500, headers: { 'Content-Type': 'application/json' } });