From 0a9fd374c9a9f81cc548bdc3656a3014b6a4ddd2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 17:45:32 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20command=20injection=20in=20github=20clone=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced `exec` with `execFile` to prevent command injection when cloning repositories. Also added validation to prevent argument injection flags from being passed via user input. Co-authored-by: bobdivx <6737167+bobdivx@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ src/pages/api/github-clone.ts | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..033f757f --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-03-01 - Command Injection in github-clone.ts +**Vulnerability:** Command injection and argument injection in `src/pages/api/github-clone.ts`. The API endpoint accepted `repoUrl` and `repoName` from JSON body and concatenated them directly into a `git clone` shell command using `exec`. A malicious user could provide a `repoName` like `; rm -rf /;` to execute arbitrary commands on the server. Furthermore, they could provide a string starting with `-` to inject flags to `git`. +**Learning:** Shell-based command execution (like `exec`) paired with user input is a critical security risk. Node's `exec` passes the entire command string to a shell, making it trivial to inject additional commands. +**Prevention:** Always use `execFile` (or `spawn`) and pass arguments as an array to ensure the system executes only the intended executable with arguments securely escaped by the OS. Additionally, explicitly validate that user-controlled input intended as arguments (like URLs or folder names) does not start with a hyphen (`-`) to prevent flag/argument injection against the target executable itself. diff --git a/src/pages/api/github-clone.ts b/src/pages/api/github-clone.ts index 5ff437b8..002e4587 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 execFilePromise = util.promisify(execFile); export const POST: APIRoute = async ({ request }) => { try { @@ -16,6 +16,15 @@ export const POST: APIRoute = async ({ request }) => { return new Response(JSON.stringify({ error: 'repoUrl et repoName requis' }), { status: 400 }); } + if (typeof repoUrl !== 'string' || typeof repoName !== 'string') { + return new Response(JSON.stringify({ error: 'Type invalide pour repoUrl ou repoName' }), { status: 400 }); + } + + // Security: Prevent argument injection + if (repoUrl.trim().startsWith('-') || repoName.trim().startsWith('-')) { + return new Response(JSON.stringify({ error: 'Format de repoUrl ou repoName 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 +42,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 execFilePromise('git', ['clone', authUrl, repoName], { cwd: reposRoot }); // Try to auto-sync it into the database try {