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

Skip to content

Fix: narrow hidden-text-strip landmark + aria-hidden allowlists#207

Merged
twschiller merged 1 commit into
mainfrom
fix/hidden-text-strip-narrow-landmark-allowlist
Jun 6, 2026
Merged

Fix: narrow hidden-text-strip landmark + aria-hidden allowlists#207
twschiller merged 1 commit into
mainfrom
fix/hidden-text-strip-narrow-landmark-allowlist

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

Summary

Closes one item from the red-team audit in #203 (High, S).

The landmark (<nav> / <aside> / <header> / <main> / <footer> + ARIA landmark roles) and aria-hidden=\"true\" subtree allowlists in hidden-text-strip were blanket exemptions in findCandidates — anything matching either was skipped before detectHiddenByCss / detectColorMatch ever ran. That preserved Amazon's <nav id=\"shortcut-menu\"> at left: -10000px (the load-bearing use case), but it also preserved any visibility: hidden / opacity: 0 / color-matched text on or under those elements — a bypass an attacker can hit by just dropping a <nav> wrapper around the payload.

Approach

Narrow both allowlists to positional hide reasons only:

  • offscreen-left
  • offscreen-top
  • text-indent
  • clip-to-zero

Those are the shapes a11y patterns legitimately use to keep content available to assistive tech without painting it. The paint-mode hides (visibility-hidden, opacity-0, font-size-0, color-match) inside a landmark or aria-hidden subtree are now stripped as injection-shaped.

The skip is moved from before detectHiddenByCss to after it, so the match reason is in hand when we decide whether the landmark / aria-hidden exemption applies.

What still preserves vs. what now strips

Pattern Before After
<nav style=\"left: -10000px\"> (Amazon shortcut menu) preserve preserve
<div aria-hidden=\"true\"><span style=\"position:absolute;left:-10000px\">…</span></div> (decorative offscreen icon label) preserve preserve
<nav style=\"visibility: hidden\">…</nav> preserve (bypass) strip
<aside style=\"color: rgb(255,255,255); background: rgb(255,255,255)\">…</aside> preserve (bypass) strip
<div aria-hidden=\"true\"><span style=\"visibility: hidden\">…</span></div> preserve (bypass) strip
<div role=\"navigation\" style=\"visibility: hidden\">…</div> preserve (bypass) strip

Test plan

  • bun run test -- hidden-text-strip — 48 cases green (41 existing + 5 new unit + 2 new property).
  • bun run preflight — codegen + Biome + ESLint + typecheck + knip + Jest 1634/1634 green.
  • Flipped test: preserves text inside aria-hidden=true ancestors (was asserting the bypass) → split into two tests covering both halves of the new invariant.
  • New unit tests: NAV + visibility:hidden → strip; ASIDE + color-match → strip; HEADER + opacity:0 → strip; role=navigation + visibility:hidden → strip; aria-hidden + offscreen → preserve.
  • New property tests pin the cross-product (positional × {landmark tag, aria-hidden subtree}) → preserve; (non-positional × same) → strip. A future tweak (new reason added without updating the allowlist set, or a check moved back into the unconditional skip block) can't quietly re-open the bypass.

🤖 Generated with Claude Code

Closes one item from the red-team audit in #203 (High, S).

The landmark (`<nav>` / `<aside>` / `<header>` / `<main>` / `<footer>`
+ ARIA-landmark-role) and `aria-hidden="true"` subtree allowlists were
blanket exemptions in `findCandidates` — anything matching either was
skipped before `detectHiddenByCss` / `detectColorMatch` ever ran. That
preserved Amazon's `<nav id="shortcut-menu">` at `left: -10000px`, which
is the load-bearing use case, but it also preserved any `visibility:
hidden` / `opacity: 0` / color-matched text on or under those elements
— a bypass an attacker can hit by just dropping a `<nav>` wrapper
around the payload.

Narrow both allowlists to positional hide reasons only (`offscreen-*`,
`text-indent`, `clip-to-zero`). Those are the shapes a11y patterns
legitimately use; the paint-mode hides (`visibility-hidden`,
`opacity-0`, `font-size-0`, `color-match`) inside a landmark or
aria-hidden subtree are now stripped as injection-shaped.

Property tests pin the cross-product so a future tweak (new reason
added without updating the allowlist set, or a check moved back into
the unconditional skip block) can't quietly re-open the bypass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@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 2:24pm

Request Review

@twschiller twschiller merged commit 59a76ce into main Jun 6, 2026
7 checks passed
@twschiller twschiller deleted the fix/hidden-text-strip-narrow-landmark-allowlist branch June 6, 2026 14:49
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