fix(redact-prepush): close the ext-diff, header-lookalike, and ref-parse bypasses

Three ways the pushed diff escaped scanning: (1) a user-level diff.external
or textconv driver replaced the diff with its own output — zero '+' lines,
so the scan saw nothing (now --no-ext-diff --no-textconv); (2) an added
content line whose text begins with "++" renders as "+++…" and the blanket
header skip dropped it (now hunk-aware header detection); (3) a pre-push
ref line that failed to parse was silently skipped, leaving that ref
unscanned (now fails closed with the offending line named).

Minimal reimplementation of the two confirmed bypasses from PR #2498 by
@lubosxyz (the full PR overlaps the chunked-scan work absorbed separately),
plus the unparseable-ref hardening.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-14 20:20:49 -07:00
co-authored by Claude Fable 5
parent 57a211e4b0
commit 888603f570
2 changed files with 67 additions and 6 deletions
+37 -6
View File
@@ -140,12 +140,28 @@ function addedLinesFor(localSha: string, remoteSha: string): string {
// +++ file header. Unified diff added lines start with a single '+'.
// Strict (#1946): a failed diff used to return "" and the push sailed
// through unscanned — fail open on the exact path the guard exists for.
const diff = gitStrict(["diff", "--unified=0", "--no-color", range]);
//
// --no-ext-diff: a user's `diff.external` driver replaces the entire diff
// with its own output — with one set, `git diff` emits zero '+' lines, so an
// unhardened scanner reads an empty diff and exits 0 on a push full of
// secrets. Reachable from ordinary user config, not hypothetical. (#2498)
// --no-textconv: a .gitattributes textconv driver can likewise rewrite
// content before we ever see it. (#2498)
const diff = gitStrict([
"diff", "--unified=0", "--no-color", "--no-ext-diff", "--no-textconv",
range,
]);
const added: string[] = [];
// Hunk-aware header skip (#2498): `+++ ` is only a FILE HEADER outside a
// hunk. Inside a hunk, an added content line whose text begins with "++"
// renders as "+++<content>" — the old blanket startsWith("+++") skip
// silently dropped exactly those lines from the scan.
let inHunk = false;
for (const line of diff.split("\n")) {
if (line.startsWith("+") && !line.startsWith("+++")) {
added.push(line.slice(1));
}
if (line.startsWith("diff --git")) { inHunk = false; continue; }
if (line.startsWith("@@")) { inHunk = true; continue; }
if (!inHunk && (line.startsWith("+++") || line.startsWith("---"))) continue;
if (line.startsWith("+")) added.push(line.slice(1));
}
return added.join("\n");
}
@@ -239,8 +255,23 @@ function main() {
const allHigh: Finding[] = [];
let mediumCount = 0;
for (const [, localSha, , remoteSha] of refs) {
if (!localSha || ZERO.test(localSha)) continue; // branch delete → nothing pushed
for (const fields of refs) {
// Fail CLOSED on a ref line we cannot parse (#2498): git hands pre-push
// exactly "<local ref> <local sha> <remote ref> <remote sha>" — anything
// else means we cannot tell WHAT is being pushed, and silently skipping
// it would leave that ref unscanned.
const [, localSha, , remoteSha] = fields;
const shaShaped = (s: string | undefined) => !!s && /^[0-9a-f]{40,64}$/i.test(s);
if (fields.length !== 4 || !shaShaped(localSha) || !shaShaped(remoteSha)) {
process.stderr.write(
"\n⛔ gstack-redact-prepush BLOCKED the push — could not parse a pre-push ref line, " +
"so its content cannot be scanned.\n" +
` line: ${JSON.stringify(fields.join(" "))}\n` +
"Bypass if you're sure: GSTACK_REDACT_PREPUSH=skip git push (or git push --no-verify)\n",
);
process.exit(1);
}
if (ZERO.test(localSha!)) continue; // branch delete → nothing pushed
let added: string;
try {
added = addedLinesFor(localSha, remoteSha || "0");