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-10-24 - Secret Leakage in Node.js child_process errors
**Vulnerability:** When executing shell commands (via `exec` or `execFile`) that contain secrets (like an embedded `oauth2:<token>@github.com` in a git clone URL), if the command fails, the `error.message` thrown by Node.js includes the full command string, exposing the secret. If this error is passed back in an API response (e.g. `return { error: error.message }`), the secret is leaked to the client.
**Learning:** Node.js `child_process` utilities include the executed command (and its arguments) in the error object when a process exits with a non-zero code. This means we must always sanitize the `error.message` before returning it in any external response.
**Prevention:** Always use regex or string replacement to redact secrets (e.g. `.replace(/oauth2:[^@]+@/g, 'oauth2:***@')`) from `error.message` when a shell command fails, or better yet, return a generic error message and log the real error server-side.
19 changes: 15 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 @@ -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 inputs don't start with a hyphen
if (repoUrl.startsWith('-') || repoName.startsWith('-')) {
return new Response(JSON.stringify({ error: 'Les paramètres ne peuvent pas commencer par un tiret' }), { status: 400 });
Comment on lines +20 to +21
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

The current validation only checks for leading hyphens to prevent flag injection. However, repoName is used in a path.join operation and as a directory argument for git clone, making it susceptible to path traversal attacks (e.g., repoName being ../../etc). Additionally, since these values come from a JSON request, their types should be verified to ensure they are strings before calling .startsWith() to avoid potential runtime errors.

    const isSafeName = typeof repoName === 'string' && /^[a-zA-Z0-9._-]{1,128}$/.test(repoName) && repoName !== '..' && repoName !== '.';
    if (typeof repoUrl !== 'string' || repoUrl.startsWith('-') || !isSafeName) {
      return new Response(JSON.stringify({ error: 'Paramètres invalides ou nom de dépôt non sécurisé' }), { status: 400 });

}

const githubToken = await getConfig('githubToken', true);
if (!githubToken || githubToken.trim() === '') {
return new Response(JSON.stringify({ error: 'Jeton GitHub manquant' }), { status: 400 });
Expand All @@ -33,7 +38,9 @@ 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 with array instead of exec with string concatenation
// to prevent command injection
const { stdout, stderr } = await execFileAsync('git', ['clone', authUrl, repoName], { cwd: reposRoot });

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

} catch (error: any) {
return new Response(JSON.stringify({ error: error.message || 'Erreur lors du clonage' }), {
// 🛡️ Security: Sanitize error message to prevent leaking GitHub token to the client
// since Node.js exec/execFile errors include the command executed
const safeErrorMsg = (error.message || 'Erreur lors du clonage').replace(/oauth2:[^@]+@/g, 'oauth2:***@');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

While redacting the token from the error message is a necessary step when using execFile, returning the raw error message to the client is still a security risk (Information Exposure). It can reveal internal paths, environment details, or software versions. It is recommended to log the full error on the server and return a generic error message to the user, as also suggested in your .jules/sentinel.md file.

    // Log the error server-side for debugging
    console.error('[github-clone] Error:', error);
    const safeErrorMsg = 'Erreur lors du clonage du dépôt. Veuillez vérifier l\'URL et le nom du dépôt.';


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