Fix: narrow hidden-text-strip landmark + aria-hidden allowlists#207
Merged
Merged
Conversation
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]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Summary
Closes one item from the red-team audit in #203 (High, S).
The landmark (
<nav>/<aside>/<header>/<main>/<footer>+ ARIA landmark roles) andaria-hidden=\"true\"subtree allowlists inhidden-text-stripwere blanket exemptions infindCandidates— anything matching either was skipped beforedetectHiddenByCss/detectColorMatchever ran. That preserved Amazon's<nav id=\"shortcut-menu\">atleft: -10000px(the load-bearing use case), but it also preserved anyvisibility: 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-leftoffscreen-toptext-indentclip-to-zeroThose 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
detectHiddenByCssto 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
<nav style=\"left: -10000px\">(Amazon shortcut menu)<div aria-hidden=\"true\"><span style=\"position:absolute;left:-10000px\">…</span></div>(decorative offscreen icon label)<nav style=\"visibility: hidden\">…</nav><aside style=\"color: rgb(255,255,255); background: rgb(255,255,255)\">…</aside><div aria-hidden=\"true\"><span style=\"visibility: hidden\">…</span></div><div role=\"navigation\" style=\"visibility: hidden\">…</div>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.preserves text inside aria-hidden=true ancestors(was asserting the bypass) → split into two tests covering both halves of the new invariant.🤖 Generated with Claude Code