From 77d5c084307bb91d7b13cfd67f79f3ce9c5ac8ad Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:29:48 +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 🚨 Severity: CRITICAL 💡 Vulnerability: Command injection in `src/pages/api/docker-logs.ts` via the `containerId` and `tail` parameters, which were unsanitized and directly concatenated into an `execSync` shell execution. 🎯 Impact: An attacker could execute arbitrary shell commands on the host by passing crafted payloads (e.g., `; rm -rf /`) or by injecting flags (e.g., `--help`). 🔧 Fix: Refactored to use `execFileAsync` which avoids shell evaluation by passing arguments explicitly as an array. Added defense-in-depth with strict regex validation for both `containerId` (rejecting special characters and starting hyphens) and `tail`. Also sanitized error leakage in catch blocks. ✅ Verification: Ran `pnpm run check` and `pnpm test` to ensure no regressions. Co-authored-by: bobdivx <6737167+bobdivx@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ src/pages/api/docker-logs.ts | 28 ++++++++++++++++++---------- 2 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 .jules/sentinel.md 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' } });