diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..c117b682 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-05-24 - [CRITICAL] Command Injection in docker logs API +**Vulnerability:** The API endpoint `src/pages/api/docker-logs.ts` utilized `execSync` with unsanitized user inputs (`id` and `tail` query parameters) concatenated directly into a shell command string, creating a critical risk of arbitrary command execution. +**Learning:** Shell strings generated with external user input inherently trust that input to not contain shell metacharacters or secondary commands (e.g., using `;` or `&&`). Relying solely on `execSync` without passing an arguments array bypasses proper encoding. +**Prevention:** Always use `execFile` (or its async wrapper) with an array of arguments for external commands instead of string concatenation. Validate and strictly cast types (e.g., parsing integers for numeric limits), sanitize error outputs to avoid stack trace leaks, and block flags from being injected by rejecting inputs starting with hyphens (`-`). diff --git a/src/pages/api/docker-logs.ts b/src/pages/api/docker-logs.ts index cb72e09e..e13cc938 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,36 @@ export const GET: APIRoute = async ({ url }) => { } const containerId = url.searchParams.get('id'); - const tail = url.searchParams.get('tail') || '100'; + const tailParam = url.searchParams.get('tail') || '100'; + + // 🛡️ Security: Validate input and prevent flag injection + if (!containerId || containerId.startsWith('-')) { + return new Response(JSON.stringify({ error: "ID du conteneur invalide ou manquant" }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } - if (!containerId) { - return new Response(JSON.stringify({ error: "ID du conteneur manquant" }), { + const tail = parseInt(tailParam, 10); + if (isNaN(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 - const command = `docker logs --tail ${tail} ${containerId}`; - let logs = []; + // 🛡️ Security: Use execFile to prevent command injection + 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]); + 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'); } else { - throw err; + throw new Error("Execution failed"); } } @@ -41,7 +53,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 }), { + // 🛡️ Security: Sanitize error message to prevent leaking system details + return new Response(JSON.stringify({ error: "Logs indisponibles" }), { status: 500, headers: { 'Content-Type': 'application/json' } });