Summary
/api/transcribe's !result.success branches return fetchTranscript's error message to the caller verbatim. That is safe only while every value it can carry is a fixed, app-authored literal. Two are not, and both are reachable by a caller who controls the request body.
This is the residue of #1383 / #1381. #1381 sanitized the thrown and JSON-parse paths and argued the remaining branches were safe because "every one of those values is an app-authored literal, a numeric HTTP status, or our own SSRF-guard message, so none is upstream text." That reasoning is correct about upstream text and correct about most branches — but it scopes the problem to the wrong category. The two values below are not upstream text; they are caller-derived, which is a different leak and is why they survived the first pass.
The two values
1. Failed to fetch audio: ${audioResponse.status} — apps/web/src/lib/transcription-service.ts
The status belongs to the caller-supplied audioUrl. The SSRF guard admits public hosts, so this returns a cross-origin read — 401 vs 403 vs 404 vs 500 for any public host the caller names — that the browser's same-origin policy would otherwise deny. A probe oracle built out of an error string.
2. The all-strategies-failed message — same file
error: hasKeys
? 'Could not transcribe video — all strategies failed'
: 'No AI API key configured. Set OPENAI_API_KEY or GEMINI_API_KEY in Vercel environment variables.',
Branching the client-facing message on hasKeys discloses whether provider keys are configured, and names both the environment variables and the hosting platform.
Also: the billing_not_configured branch in apps/web/src/app/api/transcribe/route.ts still carries details: 'Configure billing in Google Cloud and OpenAI console', naming this deployment's cloud and model vendors. #1381's description states details was removed from this route; this instance was left behind.
Severity
Moderate, not critical. /api/transcribe is not on the public allowlist in apps/web/src/lib/auth-paths.ts — it requires a session when NEXTAUTH_SECRET is configured — so unlike the billing routes #1381 patched, this is authenticated-only. It is a defense-in-depth CWE-209 gap rather than an anonymous-reachable one.
Acceptance criteria
Notes
Found while driving #1381 through the PR remediation runbook; it is the part of CodeRabbit's CHANGES_REQUESTED on that PR (review 4863120464) that is still live on its current head. The rest of that review is addressed by #1381's later commits.
Fixed by #1440, which stacks onto #1381 rather than competing with it.
Summary
/api/transcribe's!result.successbranches returnfetchTranscript's error message to the caller verbatim. That is safe only while every value it can carry is a fixed, app-authored literal. Two are not, and both are reachable by a caller who controls the request body.This is the residue of #1383 / #1381. #1381 sanitized the thrown and JSON-parse paths and argued the remaining branches were safe because "every one of those values is an app-authored literal, a numeric HTTP status, or our own SSRF-guard message, so none is upstream text." That reasoning is correct about upstream text and correct about most branches — but it scopes the problem to the wrong category. The two values below are not upstream text; they are caller-derived, which is a different leak and is why they survived the first pass.
The two values
1.
Failed to fetch audio: ${audioResponse.status}—apps/web/src/lib/transcription-service.tsThe status belongs to the caller-supplied
audioUrl. The SSRF guard admits public hosts, so this returns a cross-origin read — 401 vs 403 vs 404 vs 500 for any public host the caller names — that the browser's same-origin policy would otherwise deny. A probe oracle built out of an error string.2. The all-strategies-failed message — same file
Branching the client-facing message on
hasKeysdiscloses whether provider keys are configured, and names both the environment variables and the hosting platform.Also: the
billing_not_configuredbranch inapps/web/src/app/api/transcribe/route.tsstill carriesdetails: 'Configure billing in Google Cloud and OpenAI console', naming this deployment's cloud and model vendors. #1381's description statesdetailswas removed from this route; this instance was left behind.Severity
Moderate, not critical.
/api/transcribeis not on the public allowlist inapps/web/src/lib/auth-paths.ts— it requires a session whenNEXTAUTH_SECRETis configured — so unlike the billing routes #1381 patched, this is authenticated-only. It is a defense-in-depth CWE-209 gap rather than an anonymous-reachable one.Acceptance criteria
fetchTranscripterror value interpolates caller-supplied input or server configuration state.transcription-service.ts, not masked at the route, so other callers offetchTranscriptinherit it.Notes
Found while driving #1381 through the PR remediation runbook; it is the part of CodeRabbit's
CHANGES_REQUESTEDon that PR (review 4863120464) that is still live on its current head. The rest of that review is addressed by #1381's later commits.Fixed by #1440, which stacks onto #1381 rather than competing with it.