diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..2f7d536a --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-05-24 - DoS Risk and Info Leak in API Routes +**Vulnerability:** Synchronous shell execution (`execSync`) in API routes blocks the Node.js event loop causing Denial of Service, and unhandled errors leak internal stack traces to clients. +**Learning:** Using `execSync` on web endpoints severely degrades performance and availability. Concatenating system error messages directly into JSON responses exposes sensitive server paths and details. +**Prevention:** Always use asynchronous execution (`execFile` with promisify) and sanitize error outputs with generic messages (e.g., "Logs indisponibles") before sending HTTP responses. diff --git a/src/pages/api/forge-logs.ts b/src/pages/api/forge-logs.ts index 68638bee..a09e01a9 100644 --- a/src/pages/api/forge-logs.ts +++ b/src/pages/api/forge-logs.ts @@ -1,36 +1,52 @@ -import type { APIRoute } from 'astro'; -import { execSync } from 'child_process'; -import path from 'path'; - -export const GET: APIRoute = async ({ url }) => { - try { - const type = url.searchParams.get('type') || 'watchdog'; - const lines = parseInt(url.searchParams.get('lines') || '50', 10); - - const logFile = type === 'dashboard' ? 'dashboard.log' : 'watchdog.log'; - const filePath = path.join(process.cwd(), logFile); - - let output = ""; - try { - output = execSync(`tail -n ${lines} ${filePath}`).toString(); - } catch (e) { - output = "Erreur lors de la lecture du fichier log."; - } - - const logLines = output.trim().split('\n').reverse(); - - return new Response(JSON.stringify({ - logs: logLines, - file: logFile, - timestamp: new Date().toISOString() - }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); - } catch (error: any) { - return new Response(JSON.stringify({ error: error.message }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } -}; +import type { APIRoute } from 'astro'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import path from 'path'; + +const execFileAsync = promisify(execFile); + +export const GET: APIRoute = async ({ url }) => { + try { + const type = url.searchParams.get('type') || 'watchdog'; + // Validate inputs to prevent argument injection + const linesStr = url.searchParams.get('lines') || '50'; + const lines = parseInt(linesStr, 10); + + if (isNaN(lines) || lines <= 0) { + return new Response(JSON.stringify({ error: 'Paramètre lines invalide.' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + const logFile = type === 'dashboard' ? 'dashboard.log' : 'watchdog.log'; + const filePath = path.join(process.cwd(), logFile); + + let output = ""; + try { + // Use execFile with argument array instead of execSync to prevent command injection and event loop blocking + const { stdout } = await execFileAsync('tail', ['-n', lines.toString(), filePath]); + output = stdout; + } catch (e: any) { + // Return a purely generic message to prevent leaking absolute server paths via tail's stderr + output = "Erreur lors de la lecture du fichier log."; + } + + const logLines = output.trim().split('\n').reverse(); + + return new Response(JSON.stringify({ + logs: logLines, + file: logFile, + timestamp: new Date().toISOString() + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error: any) { + // Return generic error message to prevent leaking internal stack traces or paths + return new Response(JSON.stringify({ error: 'Erreur interne du serveur lors de la récupération des logs.' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +};