Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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).
90 changes: 59 additions & 31 deletions src/pages/api/docker-logs.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,77 @@
import type { APIRoute } from 'astro';
import { execSync } from 'child_process';
import type { APIRoute } from "astro";
import { execFileSync } from "child_process";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using synchronous process execution (execFileSync) blocks the single-threaded Node.js event loop. This can severely degrade the performance and responsiveness of the API under concurrent requests. It is highly recommended to use asynchronous execution instead.

import { execFile } from "child_process";
import { promisify } from "util";

const execFileAsync = promisify(execFile);


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");
Comment on lines +49 to +54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Refactor the synchronous execution to use the asynchronous execFileAsync to prevent blocking the event loop.

Suggested change
const output = execFileSync(
"docker",
["logs", "--tail", tail, containerId],
{ stdio: ["pipe", "pipe", "pipe"] },
).toString();
logs = output.trim().split("\n");
const { stdout } = await execFileAsync(
"docker",
["logs", "--tail", tail, containerId]
);
logs = stdout.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" },
},
);
}
};