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 @@
## 2024-05-18 - Prevent Command Injection and Token Leaks in Git Operations
**Vulnerability:** The API route `src/pages/api/github-clone.ts` used `exec` to run `git clone`, interpolating unvalidated user inputs (`repoUrl` and `repoName`) and an authentication URL that contained a sensitive GitHub token. This exposed the system to command injection risks and risked leaking the token if the shell command failed and the resulting exception message containing the command was sent to the client.
**Learning:** Using shell execution (`exec` or `execSync`) with string concatenation for commands that include secrets or user inputs is highly dangerous. A failed command will include the raw string (including the secret) in the `error.message`.
**Prevention:** Always use `execFile` or `spawn` with an array of arguments, particularly when executing system commands. Use `--` to separate flags from positional arguments, preventing argument injection. In error handling paths, deliberately sanitize `error.message` to replace any known sensitive secrets (like tokens or passwords) with placeholders (e.g., `***`) before returning the message in an API response.
15 changes: 11 additions & 4 deletions src/pages/api/github-clone.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -33,7 +33,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 });
// Use execFile with array arguments to prevent command injection
const { stdout, stderr } = await execFileAsync('git', ['clone', '--', authUrl, repoName], { cwd: reposRoot });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The repoName parameter is taken directly from the user request and is not validated before being used in path.join and passed to git clone. This can lead to a Path Traversal vulnerability, allowing an attacker to clone repositories into arbitrary directories outside of reposRoot (e.g., by using ../ in repoName).

To prevent this, validate repoName using a strict alphanumeric/safe character regex (matching the isSafeRepoDirName helper pattern) before proceeding with the clone operation.

    if (!/^[a-zA-Z0-9._-]{1,128}$/.test(repoName)) {
      return new Response(JSON.stringify({ error: 'Nom de dépôt invalide' }), { status: 400 });
    }
    const { stdout, stderr } = await execFileAsync('git', ['clone', '--', authUrl, repoName], { cwd: reposRoot });


// Try to auto-sync it into the database
try {
Expand All @@ -53,7 +54,13 @@ export const POST: APIRoute = async ({ request }) => {
});

} catch (error: any) {
return new Response(JSON.stringify({ error: error.message || 'Erreur lors du clonage' }), {
const githubToken = await getConfig('githubToken', true);
// Secure error handling: prevent leaking the github token
const safeError = error.message
? (githubToken ? error.message.replaceAll(githubToken, '***') : error.message)
: 'Erreur lors du clonage';
Comment on lines +57 to +61
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

Calling await getConfig('githubToken', true) inside the catch block is risky. If the initial error in the try block was caused by a database failure, calling getConfig again will throw another error, causing a double-fault/unhandled promise rejection and crashing the request without returning a proper JSON response.

Additionally, if githubToken is empty or undefined, calling replaceAll with an empty string will throw a TypeError in JavaScript.

Wrap the token retrieval and replacement in a try/catch block and ensure githubToken is a non-empty string before calling replaceAll.

    let safeError = error.message || 'Erreur lors du clonage';
    try {
      const githubToken = await getConfig('githubToken', true);
      if (githubToken && githubToken.trim() !== '' && error.message) {
        safeError = error.message.replaceAll(githubToken, '***');
      }
    } catch {
      // Ignore DB errors to prevent double-faulting
    }


return new Response(JSON.stringify({ error: safeError }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
Expand Down