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-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.
12 changes: 9 additions & 3 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 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 });
Expand All @@ -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 {
Expand Down