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

Skip to content

Fix: extend hidden-text-strip with six additional CSS hide paths#206

Merged
twschiller merged 3 commits into
mainfrom
fix/hidden-text-strip-additional-css-hide-paths
Jun 6, 2026
Merged

Fix: extend hidden-text-strip with six additional CSS hide paths#206
twschiller merged 3 commits into
mainfrom
fix/hidden-text-strip-additional-css-hide-paths

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

Summary

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

Six CSS hide paths flagged in the audit slipped past detectHiddenByCss. This PR adds detection for each, with the same animation-in-flight guard as the existing opacity: 0 check so transient modal-open / accordion-collapse states aren't stripped mid-animation.

Trigger Notes
-webkit-text-fill-color: transparent Gated on direct text + NOT background-clip: text so gradient-text headlines (the standard gradient-headline idiom) stay readable.
filter: opacity(0) Chains like brightness(1) opacity(0%) match. Guards against transition: filter.
mask-image: linear-gradient(transparent, transparent) Detects the mask-image longhand, the -webkit- longhand, and the mask: shorthand. Strict parser — rejects gradients with any opaque stop.
transform: scale(0) (and scaleX/scaleY/scale3d/matrix forms) Recognizes the function form (jsdom) and the resolved matrix form (real browsers). Guards against transition: transform.
content-visibility: hidden auto is a perf hint that stays visible; only hidden qualifies.
max-height: 0 + overflow: hidden Skipped for 1×1 SR-only envelopes (already preserved) and when a transition: max-height is in flight (accordion).

Notes for reviewers

  • Two cross-cutting fixes that the new branches exposed:
    • parseColor now resolves the transparent keyword. Real browsers normalize the computed value to rgba(0, 0, 0, 0) but jsdom under Jest returns the keyword verbatim, and the regex parser fell through to null.
    • parsePixelLength accepts a bare 0. Real browsers (and jsdom under Jest) drop the px suffix for any zero-valued length, so the strict ^…px$ regex rejected max-height: 0.
  • -webkit-mask-image unit test was omitted — jsdom doesn't reflect that property at all (webkitMaskImage reads back as ""). The production code still covers it because Chromium aliases the -webkit- longhand to the standard getter, and the mask: shorthand test exercises the same isFullyTransparentMask code path.
  • Gradient text preservation is verified by a unit test using background-image: + background-clip: text + -webkit-text-fill-color: transparent. jsdom does reflect background-clip: text correctly as long as the shorthand background: isn't used.

Test plan

  • bun run test -- hidden-text-strip — 65 cases green (41 existing + 20 new unit + 4 new property).
  • bun run preflight — codegen + Biome + ESLint + typecheck + knip + Jest 1651/1651 green.
  • New unit tests cover each new branch + the gradient-text false-positive guard + each animation-in-flight guard + the content-visibility: auto and max-height:0 without overflow negative cases.
  • New property tests pin the classes for the matrix → scale parser and the gradient-mask transparency parser — future tweaks to either parser can't silently miss a class of inputs.

🤖 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 2:58pm

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 on lines +301 to +303
const FILTER_PROPERTY_PATTERN = /\b(?:filter|all)\b/;
const TRANSFORM_PROPERTY_PATTERN = /\b(?:transform|all)\b/;
const COLLAPSE_PROPERTY_PATTERN = /\b(?:max-height|height|all)\b/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Because - is not a \w character, \b creates a word boundary at the hyphen in compound CSS property names. This means:

  • FILTER_PROPERTY_PATTERN (\bfilter\b) matches filter inside backdrop-filter. An attacker can add transition: backdrop-filter 999s to suppress detection of filter: opacity(0).
  • COLLAPSE_PROPERTY_PATTERN (\bheight\b) matches height inside line-height. An attacker can add transition: line-height 999s to suppress detection of max-height: 0; overflow: hidden.

Both allow the animation-in-flight guard to fire on an unrelated property, bypassing the hidden-text strip entirely.

Fix by using a CSS-property-aware boundary instead of \b. For example, using a negative lookbehind for [a-z-]:

const FILTER_PROPERTY_PATTERN = /(?<![a-z-])(?:filter|all)(?![a-z-])/;
const COLLAPSE_PROPERTY_PATTERN = /(?<![a-z-])(?:max-height|height|all)(?![a-z-])/;

OPACITY_PROPERTY_PATTERN and TRANSFORM_PROPERTY_PATTERN happen to be safe today because no standard CSS property ends with -opacity or -transform, but applying the same fix to all four would be defensive.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in c301765. Verified the bypass first:

> /\b(?:filter|all)\b/.test("backdrop-filter")        // true
> /\b(?:max-height|height|all)\b/.test("line-height") // true

Switched all four property patterns to (?<![a-z-])…(?![a-z-]) per your suggestion — even the two that are safe today (OPACITY, TRANSFORM) since the boundary should be one consistent thing across the file. Added two regression tests pinning the concrete bypasses (transition: backdrop-filter 999s no longer suppresses the filter: opacity(0) strip; transition: line-height 999s no longer suppresses the max-height: 0 strip). Thanks for the careful read.

— Claude Code, on behalf of @twschiller

twschiller and others added 3 commits June 6, 2026 10:55
Closes one item from the red-team audit in #203 (High, S). Adds detection
for the CSS hide paths flagged in the audit that previously slipped past
`detectHiddenByCss`:

- `-webkit-text-fill-color: transparent` (gated on direct text + not
  `background-clip: text` so gradient-text headlines stay readable)
- `filter: opacity(0)` including chains like `brightness(1) opacity(0)`
- `mask-image: linear-gradient(transparent, transparent)` and `mask:`
  shorthand / `-webkit-mask-image` variants
- `transform: scale(0)` (and `scaleX`/`scaleY`/`scale3d` / matrix forms)
- `content-visibility: hidden`
- `max-height: 0` + `overflow: hidden` on non-1×1 elements

Each animatable trigger gets the same animation-in-flight guard as the
existing `opacity: 0` check so transient modal-open / accordion-collapse
states aren't stripped mid-animation.

Includes unit cases for each new branch + 4 property tests pinning the
classes (every collapsing transform form strips, every non-collapsing
one preserves; every all-transparent gradient mask strips, every
gradient with an opaque stop preserves).

Also includes two cross-cutting fixes that the new branches exposed:
- `parseColor` now resolves the `transparent` keyword (jsdom under Jest
  returns the keyword verbatim instead of normalizing to rgba).
- `parsePixelLength` accepts a bare `0` (computed values for zero-length
  drop the `px` suffix in real browsers and in jsdom-under-Jest).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
CSS property names contain `-`, which is not a `\w` character, so `\b`
finds a word boundary at the hyphen. That made `\bfilter\b` match
`filter` inside `backdrop-filter` and `\bheight\b` match inside
`line-height` — an attacker could declare `transition: backdrop-filter
999s` or `transition: line-height 999s` to trip the animation-in-flight
guard and bypass the strip entirely.

Switch all four property patterns to `(?<![a-z-])…(?![a-z-])` per
reviewer suggestion. `OPACITY` and `TRANSFORM` were safe today
(no standard CSS property ends in `-opacity` or `-transform`) but use
the same boundary for consistency.

Regression tests pin both concrete bypasses.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Generalize the two concrete bypass regression tests (`transition:
backdrop-filter` no longer suppresses `filter: opacity(0)`; `transition:
line-height` no longer suppresses `max-height: 0`) into a property
test that exercises eight CSS property names containing each guard
keyword as a substring: `-webkit-filter`, `min-height`, `transform-
origin`, `transform-style`, `fill-opacity`, `stroke-opacity`, plus the
two from the unit tests.

A future tweak that drops the hyphen-aware boundary back to `\b` (or
adds a new guard keyword without the same boundary) can't quietly
re-open the bypass class — the property fails on the first regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@twschiller twschiller force-pushed the fix/hidden-text-strip-additional-css-hide-paths branch from c301765 to e2760ce Compare June 6, 2026 14:58
@twschiller twschiller merged commit 4d72d93 into main Jun 6, 2026
7 checks passed
@twschiller twschiller deleted the fix/hidden-text-strip-additional-css-hide-paths branch June 6, 2026 15:04
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