map: match full input paths and bind flows to the exact sink call - #123
Merged
Conversation
…eal Workers bundler Closes the last gap from the external review: our edge coverage bundled dist/protect.edge.js DIRECTLY, which proves the artifact is edge-clean but not that a consumer ever reaches it — a mis-ordered or mistyped `exports` condition would silently hand an edge bundler the Node build. - tests/protect/edge-export-resolution.test.ts imports the real specifier (`@patchstack/connect/protect`) from a fixture with the package linked into node_modules, and resolves it under workerd / worker / edge-light / deno / browser, asserting each lands on the edge artifact. The CONTROL is what makes it meaningful: with no edge condition the same import resolves to the Node build and FAILS to bundle for a Node-free target, so a pass is caused by the condition rather than a lenient target. Mutation-checked: pointing `workerd` at dist/protect.js makes it fail. (platform 'neutral' on purpose — 'browser' would inject the `browser` condition and mask whether the edge conditions themselves work.) - scripts/verify-edge-platform.mjs (`npm run verify:edge`) compiles a real Worker with the actual Cloudflare toolchain (`wrangler deploy --dry-run`) and asserts wrangler selected dist/protect.edge.js and emitted a bundle with no Node builtins. Verified locally: wrangler 4 compiles it, 134.90 KiB, zero Node imports. It downloads wrangler and shells out to a platform bundler, so it is deliberately NOT in `npm test` (which CI runs on four Node versions) — the two suite-level tests cover the same property cheaply and this is the end-to-end confirmation for a release job. A native `next build` fixture remains the one uncovered variant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two blockers an external review named before `map` can drive automatic rule
generation. Both let `precise` — the signal a generator would PIN A PARAMETER on —
identify the wrong input.
1. PATH COLLISIONS. Matching reduced inputs and reads to their last segment, so
`billing.email` and `shipping.email` both became `email`: reading one marked BOTH
precise. Flows now compare canonical full paths (`data.shipping.email` →
`shipping.email`), with index tokens erased so `tags[0].label`, `tags[].label` and
`tags.label` compare equal. `precise` requires the whole path to match, or the input to
be an ANCESTOR of what was read (`billing` legitimately covers `billing.email`).
2. SINK OWNERSHIP. Evidence was gathered from the enclosing statement, so a sibling
expression could lend it:
await Promise.all([ audit(data.title), db.from("items").insert({ title: "system" }) ]);
reported `title -> insert [precise]` although the insert only receives a literal.
Sinks now carry their source span (`start`/`end`) as their IDENTITY, and evidence comes
only from that call's own arguments plus other calls in the SAME fluent chain
(`.update({…}).eq('id', data.id)` is one operation). Walking up stops at an array
literal or argument position, which is what excludes the sibling. Note a line is not an
identity, and neither is a start offset alone: in `db.from(t).insert(x)` both calls start
at `db`, so the span key is start+end.
Also fixed two recall bugs found while testing, both of which silently produced ZERO
precise flows for the most common shapes:
- a validated payload (`Schema.parse(await req.json())`) was not treated as tainted, so
every validated handler lost its flows. Validation is NOT sanitization — a validated
value is still attacker-controlled — so taint now survives `parse`/`safeParse`.
- a destructured handler param (`{ data }`, TanStack's validated-payload convention) was
treated as a path SEGMENT rather than a container, shifting every path by one and
killing all matching.
Sinks now expose `start`/`end` in the emitted map, as the review asked, so a consumer can
point at the exact call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Precise span-based flow binding greatly improves path matching accuracy. 🎯 Quality: 80% Excellent · 📦 Size: Large — consider splitting if possible 🛡️ Standards: no pre-flight fit check ran for this change — wire 📈 This month: Your 56th PR — above team average · Averaging Good |
Contributor
Author
|
/review |
daniloradovic
approved these changes
Aug 13, 2026
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 two blockers an external review named before
mapcan be used as an automatic rule-authoring source. Both letprecise— the signal a generator would pin a parameter on — identify the wrong input.1. Path collisions
Matching reduced both inputs and reads to their last segment, so
billing.emailandshipping.emailboth becameemail: reading one marked both precise.Flows now compare canonical full paths (
data.shipping.email→shipping.email), with index tokens erased sotags[0].label,tags[].labelandtags.labelcompare equal.preciserequires the whole path to match, or the input to be an ancestor of what was read (billinglegitimately coversbilling.email).2. Sink ownership
Evidence was gathered from the enclosing statement, so a sibling expression could lend it:
…reported
title → insert [precise]although the insert only receives a literal.Sinks now carry their source span (
start/end) as their identity, and evidence comes only from that call's own arguments plus other calls in the same fluent chain (.update({…}).eq('id', data.id)is one operation). Walking up stops at an array literal or argument position — which is exactly what excludes the sibling.Two subtleties worth noting: a line is not an identity, and neither is a start offset alone — in
db.from(t).insert(x)both calls start atdb, so the key isstart:end.Two recall bugs found while testing
Both silently produced zero precise flows for the most common shapes:
Schema.parse(await req.json())broke the chain, so every validated handler lost its flows. Validation is not sanitization — a validated value is still attacker-controlled — so taint now survivesparse/safeParse.{ data }(TanStack's validated-payload convention) shifted every path by one segment, killing all matching.Result on the real reference app
More accurate than before — the spurious helper-reached
selectflows are gone, and the chain-fed filters are correctly precise:Sinks also expose
start/endin the emitted map, as the review asked, so a consumer can point at the exact call. 784 tests green; typecheck clean.Not in this PR
The review's larger rule-generation roadmap — argument roles (command vs args, URL vs body, path vs contents, raw SQL vs values), the finer confidence taxonomy (
exact/transformed/interprocedural/heuristic/unknown), a TypeChecker-based dataflow engine, explicit why-not-generatable reporting, the candidate lifecycle, and corpus metrics. Those are the next tranche, tracked separately; this PR only removes the two correctness blockers.