diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..83720651 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-05-06 - [Critical Command Injection in git clone] +**Vulnerability:** Command injection and argument injection in `src/pages/api/github-clone.ts`. +**Learning:** `child_process.exec()` was used with interpolated user input (`repoUrl` and `repoName`), which allows arbitrary shell commands. Even after replacing it with `execFile`, there's still a risk of flag/argument injection if a user supplies a `repoName` starting with a hyphen (e.g., `-o`). +**Prevention:** Always use `execFile` or `spawn` instead of `exec` to prevent shell injection. Additionally, validate that user inputs passed as positional arguments do not start with `-` to prevent argument injection. diff --git a/src/pages/api/github-clone.ts b/src/pages/api/github-clone.ts index 5ff437b8..96fcaf49 100644 --- a/src/pages/api/github-clone.ts +++ b/src/pages/api/github-clone.ts @@ -1,12 +1,12 @@ import type { APIRoute } from 'astro'; -import { exec } from 'node:child_process'; +import { execFile } from 'node:child_process'; import util from 'node:util'; import fs from 'node:fs'; import path from 'node:path'; import { getReposRootResolved } from '../../lib/forge-repos'; import { getConfig } from '../../lib/config-db'; -const execPromise = util.promisify(exec); +const execFileAsync = util.promisify(execFile); export const POST: APIRoute = async ({ request }) => { try { @@ -16,6 +16,11 @@ export const POST: APIRoute = async ({ request }) => { return new Response(JSON.stringify({ error: 'repoUrl et repoName requis' }), { status: 400 }); } + // 🛡️ Security: Prevent flag injection by ensuring repoName doesn't start with a hyphen + if (repoName.startsWith('-')) { + return new Response(JSON.stringify({ error: 'Nom de dépôt invalide' }), { status: 400 }); + } + const githubToken = await getConfig('githubToken', true); if (!githubToken || githubToken.trim() === '') { return new Response(JSON.stringify({ error: 'Jeton GitHub manquant' }), { status: 400 }); @@ -33,7 +38,8 @@ export const POST: APIRoute = async ({ request }) => { const authUrl = repoUrl.replace('https://', `https://oauth2:${githubToken}@`); // Clone the repository - const { stdout, stderr } = await execPromise(`git clone ${authUrl} ${repoName}`, { cwd: reposRoot }); + // 🛡️ Security: Use execFile to prevent command injection + const { stdout, stderr } = await execFileAsync('git', ['clone', authUrl, repoName], { cwd: reposRoot }); // Try to auto-sync it into the database try {