From 4b874c4237bea951ff47b5c94946f743b006632f Mon Sep 17 00:00:00 2001 From: atul-upadhyay-7 Date: Sun, 21 Jun 2026 16:25:10 +0530 Subject: [PATCH] fix: prevent GitHub PAT leak and move Gemini API key to header in architecture endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The POST /api/architecture endpoint embedded the server's GitHub PAT directly into the git clone URL (x-access-token:{token}@github.com/...). When clone failed, git's stderr output included the full URL with the token, which was logged verbatim via console.error — leaking the PAT to server logs accessible to anyone with log access. Fixes: 1. Uses GIT_ASKPASS to provide credentials to git instead of embedding them in the URL, keeping the token out of process arguments and error output. 2. Adds sanitizeError() to strip x-access-token patterns from error messages before logging. 3. Moves GEMINI_API_KEY from URL query parameter to x-goog-api-key request header to prevent exposure to proxy/CDN logs. Fixes #6233 Human Coded --- app/api/architecture/route.ts | 37 +++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/app/api/architecture/route.ts b/app/api/architecture/route.ts index 39e09c0a5..db7692452 100644 --- a/app/api/architecture/route.ts +++ b/app/api/architecture/route.ts @@ -10,6 +10,15 @@ import { getGitHubTokens } from '@/lib/github'; const execFilePromise = promisify(execFile); +/** + * Strips credentials (x-access-token:...@) from error messages to prevent + * leaking tokens into server logs. + */ +function sanitizeError(err: unknown): string { + const msg = err instanceof Error ? err.message : String(err); + return msg.replace(/x-access-token:[^@]+@/g, 'x-access-token:[REDACTED]@'); +} + // Supported files for parsing imports/exports const PARSABLE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']); // Supported text files to show in folder tree @@ -273,21 +282,30 @@ export async function POST(req: NextRequest) { const { owner, repo } = repoDetails; - // Construct authenticated clone URL if GITHUB_TOKEN is available + // Construct clone URL — never embed credentials in the URL string. + // If a token is available, use GIT_ASKPASS to provide it securely so it + // never appears in process arguments, shell history, or error output. const tokens = getGitHubTokens(); const token = tokens.length > 0 ? tokens[0] : null; - const cloneUrl = token - ? `https://x-access-token:${token}@github.com/${owner}/${repo}.git` - : `https://github.com/${owner}/${repo}.git`; + const cloneUrl = `https://github.com/${owner}/${repo}.git`; // Create a temporary directory tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `commitpulse-arch-${owner}-${repo}-`)); // Shallow clone the repository try { - await execFilePromise('git', ['clone', '--depth', '1', '--', cloneUrl, tempDir]); + const env = { ...process.env } as NodeJS.ProcessEnv; + if (token) { + // GIT_ASKPASS points to a script that echoes the token when git asks + // for credentials, keeping the token out of process arguments. + const askpassScript = path.join(tempDir, '.git-askpass.sh'); + fs.writeFileSync(askpassScript, `#!/bin/sh\necho "${token}"`, { mode: 0o700 }); + env.GIT_ASKPASS = askpassScript; + env.GIT_TERMINAL_PROMPT = '0'; + } + await execFilePromise('git', ['clone', '--depth', '1', '--', cloneUrl, tempDir], { env }); } catch (err) { - console.error('Cloning failed for repository:', repoUrl, err); + console.error('Cloning failed for repository:', repoUrl, sanitizeError(err)); // Clean up tempDir if it was created if (tempDir && fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); @@ -593,10 +611,13 @@ export async function POST(req: NextRequest) { Return exactly 5 bullet points. Do not include a conversational introduction or outro. `; - const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiApiKey}`; + const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent`; const response = await fetch(geminiUrl, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'x-goog-api-key': geminiApiKey, + }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { responseMaxOutputTokens: 500 },