Feat: hidden-affiliate-sanitize rule for affiliate/UTM/referral metadata (#121)#188
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
1 issue found.
About Unblocked
Unblocked has been set up to automatically review your team's pull requests to identify genuine bugs and issues.
📖 Documentation — Learn more in our docs.
💬 Ask questions — Mention @unblocked to request a review or summary, or ask follow-up questions.
👍 Give feedback — React to comments with 👍 or 👎 to help us improve.
⚙️ Customize — Adjust settings in your preferences.
20e7631 to
d32dbf0
Compare
…#121) Narrow sanitize companion to form-prefill-annotate (PR1). On checkout URLs, clear `value` on `<input type="hidden">` whose `name` matches a curated affiliate / UTM / promo / coupon allowlist (utm_*, aff_*, ref_*, referral_*, promo*, coupon*, discount_code, source_id, campaign_*, partner_*, click_id, gclid, fbclid, msclkid). Annotation is the wrong tool for hidden inputs — the value is submitted regardless of any chip and never reaches the agent's snapshot, so clearing is the only useful action. FP control: - Hard denylist takes precedence (CSRF / nonce / signature / hmac / secret / session / state / cart_id / order_id / token / antiforgery / verification / authenticity, etc.). Substring-and-whole-name match so `_csrf`, `csrf-token`, `apple_pay_signature`, and `_token` are all preserved. Failure mode for these is a silently-rejected submit — strictly worse than the original dark pattern. - URL gate via isCheckoutUrl. - Form-scoped: the input must live inside an enclosing `<form>` (or reference one via the `form` attribute). Free-floating hidden inputs are JS-only carriers we leave alone. - Per-host kill-switch (empty at launch) for known loyalty / Apple Pay / 1-Click flows where saved attribution is the user intent. - Set-once: stamping CLEARED_ATTR prevents a re-clear fight loop with pages that repopulate the value after our scan. Value is cleared via the prototype's native setter so React/Vue value trackers see the change. No input/change event dispatch — hidden inputs don't expect them and firing one risks tripping totals-recalculation that re-fetches attribution from the same source. Ships default-on. Tests cover the curated allowlist, denylist precedence, form-scope requirement, lazy-loaded subtrees, URL gate, form-attribute association, already-empty short-circuit, and the no- double-clear guarantee. Property tests assert allowlist/denylist disjointness, denylist precedence across composed names, URL-gate invariance, and idempotency across a mix of allowed and preserved inputs. Docs updated under Dark patterns → Preselection alongside form-prefill- annotate; demo Checkout page grows hidden affiliate / UTM / promo metadata plus a csrf_token so both halves of the rule are exercised. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
d32dbf0 to
ac285e7
Compare
When the watcher delivers a single `<input type="hidden">` appended directly to an existing form, that input is itself the root passed to `onSubtrees`. `querySelectorAll` only walks descendants, so the input was never caught. Extract the per-element scrub into a `tryClearInput` helper and invoke it once for the root before the descendant loop — same root+descendants pattern as `attribute-injection-sanitize` and `confirmshame-sanitize`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Drop `promo` / `promotion` / `coupon` / `discount` shapes from the affiliate allowlist. Hidden promo-code inputs commonly carry a legitimate user-acquired discount (email promo link, sticky session promo, "apply coupon" UI that writes to a hidden field at submit time); clearing them would silently strip the user's discount with no visible recourse. Attribution (UTM, gclid, affiliate refs) has the opposite asymmetry — clearing it is invisible to the user and only costs the marketing trail. Tests: - Move promo/coupon/discount names from positive to negative isAffiliateName cases. - New shouldClearName preservation suite (it.each over the seven promo/coupon/discount shapes). - New rule-level it.each that asserts a hidden promo input survives alongside a cleared utm_source. - New property test: no PROMO_NAMES match the allowlist or get cleared, and a hidden promo input at checkout keeps its value. Rule label tightened to "Scrub Hidden Affiliate Metadata" (no longer "… / Promo Metadata"); demo Checkout.tsx comment + docs/rules.md section updated to reflect the preservation rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
DENY_TOKENS header comment cited `token` as a substring entry and `coupon_token`/`aff_token` as denylist examples — both are stale post round-2 (token is whole-name-only; coupon was removed from the allowlist). Rewrote the rationale to a hypothetical that the substring denylist actually catches (`affiliate_nonce`). Renamed `never clears a denylisted name that happens to overlap the allowlist` — `state` isn't in the allowlist regex, so there's no overlap to resolve; the test is really demonstrating DENY_WHOLE_NAMES coverage. Added `verify` to the property test's SECURITY_TOKENS sample so the denylist-precedence property exercises another substring-deny token. — Claude Code, on behalf of @twschiller Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
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.
Sibling to #187 (
form-prefill-annotate), which has now landed onmain.Summary
form-prefill-annotate. On checkout URLs, clearsvalueon<input type="hidden">whosenamematches a curated affiliate / UTM / referral attribution allowlist (utm_*,aff*,ref*,source_id,campaign_*,partner_*,click_id,gclid/fbclid/msclkid).<form>), URL-gated, per-host kill-switch (empty at launch), set-once via CLEARED_ATTR so a page that repopulates the value doesn't drive a re-clear fight loop.Review rounds
2d909a4) — Fixed unblocked's catch thatquerySelectorAllskips the root element, so a single hidden input appended directly into an existing form was never cleared. Extracted per-input scrub intotryClearInputand call it for the root before the descendant loop, matchingattribute-injection-sanitize/confirmshame-sanitize. Regression test added.e90533dffb3ed520150271db827ea107e898a28d) — Dropped promo/coupon/discount from the allowlist (see Summary). Moved those names to negativeisAffiliateNamecases, added ashouldClearNamepreservation suite, an end-to-endit.eachthat survives a hidden promo input next to a cleared utm_source, and a property test asserting hidden promo inputs at checkout keep their value. Rule label tightened to "Scrub Hidden Affiliate Metadata"; demo and rules.md updated.Rebase notes
origin/main(4ac452e) after Feat: form-prefill-annotate rule for preselected form controls (#121) #187 squash-merged. Dropped the two stacked PR1 commits —git diff 1344f0b origin/main(PR1 HEAD vs squash-merge result) was empty for source files, so the rebase replayed only the PR2 commit on top of the new main with no conflicts.actions/checkout, Bump astral-sh/setup-uv from 7 to 8.1.0 #193setup-uv, Bump marocchino/sticky-pull-request-comment from 2 to 3 #195sticky-pull-request-comment) are dependabot workflow bumps with no file overlap.Test plan
bun run test(1616 tests passing)bun run checkbun run typecheckpre-commit runon changed files (demo-site eslint skipped locally — no node_modules in this worktree; CI runs it)🤖 Generated with Claude Code