-
Notifications
You must be signed in to change notification settings - Fork 0
🛡️ Sentinel: [CRITICAL] Fix command injection and secret leak in github-clone #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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 { | ||
|
|
@@ -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 }); | ||
| } | ||
|
|
||
| const githubToken = await getConfig('githubToken', true); | ||
| if (!githubToken || githubToken.trim() === '') { | ||
| return new Response(JSON.stringify({ error: 'Jeton GitHub manquant' }), { status: 400 }); | ||
|
|
@@ -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 { | ||
|
|
@@ -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:***@'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While redacting the token from the error message is a necessary step when using // 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' }, | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current validation only checks for leading hyphens to prevent flag injection. However,
repoNameis used in apath.joinoperation and as a directory argument forgit clone, making it susceptible to path traversal attacks (e.g.,repoNamebeing../../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.