Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Feat: hidden-affiliate-sanitize rule for affiliate/UTM/referral metadata (#121)#188

Merged
twschiller merged 4 commits into
mainfrom
hidden-affiliate-sanitize
Jun 6, 2026
Merged

Feat: hidden-affiliate-sanitize rule for affiliate/UTM/referral metadata (#121)#188
twschiller merged 4 commits into
mainfrom
hidden-affiliate-sanitize

Conversation

@twschiller

@twschiller twschiller commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Sibling to #187 (form-prefill-annotate), which has now landed on main.

Summary

  • Narrow sanitize companion to form-prefill-annotate. On checkout URLs, clears value on <input type="hidden"> whose name matches a curated affiliate / UTM / referral attribution allowlist (utm_*, aff*, ref*, source_id, campaign_*, partner_*, click_id, gclid / fbclid / msclkid).
  • Promo / coupon / discount names are intentionally preserved. 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 has the opposite asymmetry — invisible to the user, only costs the marketing trail.
  • CSRF / nonce / signature / hmac / session / state / cart_id / order_id / token / antiforgery names are preserved by a hard denylist that takes precedence.
  • Form-scoped (must live inside or reference a <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.
  • Cleared via the prototype's native value setter so React/Vue value trackers observe the change; no input/change event dispatch.
  • Default-on. Docs section under Dark patterns → Preselection; demo Checkout page exercises both halves (utm/aff cleared, coupon_code + csrf_token preserved).

Review rounds

  • Round 1 (2d909a4)Fixed unblocked's catch that querySelectorAll skips the root element, so a single hidden input appended directly into an existing form was never cleared. Extracted per-input scrub into tryClearInput and call it for the root before the descendant loop, matching attribute-injection-sanitize / confirmshame-sanitize. Regression test added.
  • Round 2 (e90533dffb3ed520150271db827ea107e898a28d) — Dropped promo/coupon/discount from the allowlist (see Summary). Moved those names to negative isAffiliateName cases, added a shouldClearName preservation suite, an end-to-end it.each that 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

Test plan

  • bun run test (1616 tests passing)
  • bun run check
  • bun run typecheck
  • pre-commit run on changed files (demo-site eslint skipped locally — no node_modules in this worktree; CI runs it)
  • Catalog invariants pass
  • Property tests assert allowlist/denylist disjointness, denylist precedence across composed names, URL-gate invariance, idempotency across a mix of allowed and preserved inputs, and that promo/coupon/discount fields survive scrubbing at checkout

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-browser-shield-demo-site Ready Ready Preview, Comment Jun 6, 2026 12:35pm

Request Review

@unblocked unblocked Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread extension/src/rules/hidden-affiliate-sanitize.ts
@twschiller twschiller force-pushed the hidden-affiliate-sanitize branch from 20e7631 to d32dbf0 Compare June 6, 2026 11:48
Base automatically changed from form-prefill-annotate to main June 6, 2026 12:01
…#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]>
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]>
@twschiller twschiller changed the title Feat: hidden-affiliate-sanitize rule for affiliate/UTM/promo metadata (#121) Feat: hidden-affiliate-sanitize rule for affiliate/UTM/referral metadata (#121) Jun 6, 2026
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]>
@twschiller twschiller merged commit a7e315a into main Jun 6, 2026
7 checks passed
@twschiller twschiller deleted the hidden-affiliate-sanitize branch June 6, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant