diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..ea4c2676 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-02-20 - Command Injection via execSync in API Routes +**Vulnerability:** A critical command injection vulnerability existed in `src/pages/api/docker-logs.ts` where unvalidated query parameters (`id` and `tail`) were directly interpolated into an `execSync` shell command string. +**Learning:** Using `exec` or `execSync` with dynamically constructed strings from user inputs inherently creates shell injection risks, even when attempting to parse them. +**Prevention:** Always use safe process execution methods like `execFile`, `execFileSync`, or `spawn` that accept command arguments as arrays. Additionally, enforce strict input validation (e.g., regex checking for alphanumeric characters and rejecting inputs starting with hyphens to prevent flag injection). diff --git a/src/pages/api/docker-logs.ts b/src/pages/api/docker-logs.ts index cb72e09e..8c92e989 100644 --- a/src/pages/api/docker-logs.ts +++ b/src/pages/api/docker-logs.ts @@ -1,49 +1,77 @@ -import type { APIRoute } from 'astro'; -import { execSync } from 'child_process'; +import type { APIRoute } from "astro"; +import { execFileSync } from "child_process"; export const GET: APIRoute = async ({ url }) => { try { const isVercel = !!process.env.VERCEL || !!process.env.VERCEL_ENV; if (isVercel) { - return new Response(JSON.stringify({ logs: ["Docker logs non disponibles sur Vercel"] }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); + return new Response( + JSON.stringify({ logs: ["Docker logs non disponibles sur Vercel"] }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); } - const containerId = url.searchParams.get('id'); - const tail = url.searchParams.get('tail') || '100'; + 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" }), { - status: 400, - headers: { 'Content-Type': 'application/json' } - }); + if ( + !containerId || + typeof containerId !== "string" || + !/^[a-zA-Z0-9_-]+$/.test(containerId) || + containerId.startsWith("-") + ) { + return new Response( + JSON.stringify({ error: "ID du conteneur manquant ou 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 = []; + if (typeof tail !== "string" || !/^\d+$/.test(tail)) { + 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 de façon sécurisée + let logs: string[] = []; try { - const output = execSync(command, { stdio: ['pipe', 'pipe', 'pipe'] }).toString(); - logs = output.trim().split('\n'); + // [Sentinel] Fix command injection by using execFileSync and input validation + const output = execFileSync( + "docker", + ["logs", "--tail", tail, containerId], + { stdio: ["pipe", "pipe", "pipe"] }, + ).toString(); + logs = output.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 { - throw err; - } + // 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 { + throw err; + } } - return new Response(JSON.stringify({ logs }), { - status: 200, - headers: { 'Content-Type': 'application/json' } + return new Response(JSON.stringify({ logs }), { + status: 200, + headers: { "Content-Type": "application/json" }, }); } catch (error: any) { - return new Response(JSON.stringify({ error: "Logs indisponibles: " + error.message }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); + return new Response( + JSON.stringify({ error: "Logs indisponibles: " + error.message }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); } };