OPU-32: BRIDGEHEAD (XOR decode-to-exec, async/PowerShell cradle) - #100
Merged
Conversation
CloudSEK's BRIDGEHEAD writeup describes an npm typosquat campaign whose dropper defeats depSNORT's existing markers two ways: a hand-rolled XOR-over-fromCharCode decode loop (not base64/hex/gzip, not the existing batch-assembly shapes), and a fetch-then-exec cradle in two JS-native shapes cradleRe's single-line shell-pipe idiom can't see — a callback-based https.get(url, () => spawn(dest)) on native Windows, and inside WSL a PowerShell command assembled at runtime from decoded fragments, so the actual cradle never exists as literal source text at all. A byte-faithful reproduction scored exit 0 on default flags and topped out at exit 2 even under -fail-on-eligible. Three new zero-execution/regex markers (D-04): xorCharCodeRe (a ^ inside a fromCharCode argument list, gated so an unrelated XOR elsewhere doesn't match); asyncCradleRe (a network fetch with an exec-family token in the SAME statement, bounded at the next ';', deliberately scoped to avoid the D-25/D-28 esbuild FP); and interpreterSpawnRe + co-occurrence (a spawned script interpreter only escalates to a cradle when CapObfuscation is also present in the hook — scores the indirection since the runtime-assembled command can't be resolved statically). Found and closed a gap in the handoff's own test suite during review: its asyncCradleRe negative used a synthetic downloadPrebuiltBinary() wrapper that never reaches a matched network verb, so it never actually exercised the same-statement boundary. Added a test against the real esbuild_regression_test.go fixture instead (fetch in one function, exec in a separate later one) — passes. Swept testdata/ for any existing fixture combining an interpreter spawn with a decode token: none found. Live end-to-end: exit 0 -> exit 1 (VC-002f block/critical, VC-002e gate-eligible/high) on a byte-faithful reproduction, matching the handoff's claimed table. Full suite green (34 packages), -race clean. Records D-124. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PLd1shywzWPsLgkpLxEyPj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
CloudSEK's BRIDGEHEAD writeup (Aug 20 2026) describes an npm typosquatting campaign whose 40-package dropper (
scripts/postinstall.js) does two things depSNORT's install-surface analyzer didn't catch:fromCharCodeloop — not base64/hex/gzip, not the existing batch-assembly shapes (onefromCharCodecall per cipher-loop iteration).cradleRe's single-line shell-pipe idiom (curl ... | sh) can't see: a callback-basedhttps.get(url, () => spawn(dest))on native Windows, and — inside WSL — a PowerShell command assembled at runtime from decoded fragments, so the actual cradle never exists as literal source text for any regex to match, encoded or not.A byte-faithful reproduction (live IOCs replaced with RFC 5737 /
.invalidplaceholders) scored exit 0 on default flags and topped out at exit 2 (gate-eligible, never flagged) even under-fail-on-eligible— no flag combination pre-patch blocked it.The fix
Three new zero-execution/regex markers in
internal/installsurface/analyze.go(D-04 — detecting the obfuscation IS the finding):xorCharCodeRe— a^inside afromCharCode(...)argument list. Gated tightly so an XOR used for something unrelated elsewhere in the file doesn't match. SetsCapObfuscation.asyncCradleRe— a network fetch with an exec-family token as an argument within the same statement (bounded at the next;). The JS-native analog ofcurl ... | sh. Deliberately same-statement-scoped to avoid re-triggering the D-25/D-28 esbuild FP (fetch in one statement, exec of an already-downloaded binary in an unrelated later one). SetsCapCradle→ VC-002f, block-tier.interpreterSpawnRe+ co-occurrence — a spawned script interpreter (powershell/pwsh/cmd/wscript/cscript/mshta) isCapExecalone; it only escalates toCapCradlewhenCapObfuscationis also present in the same hook. This closes the WSL branch: it can't decode a runtime-assembled command (D-04 forbids trying), so it scores the co-occurrence of "decodes something" + "hands something to a script interpreter" as the finding itself.Review finding — a gap in the handoff's own test suite
The shipped
asyncCradleRenegative test used a syntheticdownloadPrebuiltBinary(url)wrapper, which never reaches any of the four matched network verbs (https.get/request,fetch,axios.get/post) — so it never actually exercised the same-statement boundary it claimed to guard. Closed by adding a test against the realesbuild_regression_test.gofixture (fetch in one function,execFileSyncin a separate later one) instead of a stand-in — it passes, confirming the marker is sound against the actual reference FP case, not just a synthetic one.Also swept the full
testdata/corpus for any existing fixture combining an interpreter spawn with a decode-ish token (the FP risk the handoff itself flags for the co-occurrence check): none found.Validation
opu32_test.go), all positive/negative pairs, plus the added real-esbuild-fixture regression.-raceclean,go vetsilent,gofmtno diffs.Residual, disclosed rather than silently accepted
interpreterSpawnReco-occurrence is a heuristic (decode + separate interpreter-spawn scored together, not a resolved cradle) — worth a pass against a larger live-fire corpus if FP reports surface.asyncCradleRe's bounded window is a heuristic bound, not an AST-balanced parser (RE2 has no backreferences/lookaround).['193','70',...].join('.')) was investigated and confirmed not a gap —CapNetworkdetection was never string-literal-only.CapEnv); a dedicated WSL-fingerprint + payload-fetch/exec co-occurrence check is scoped out as a future increment.Files
internal/installsurface/analyze.go— the three markers.internal/installsurface/opu32_test.go— tests, plus the added real-esbuild-fixture regression.docs/DECISIONS.md— D-124.🤖 Generated with Claude Code
Generated by Claude Code