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

Skip to content

Fix: resolve modern CSS color syntaxes in hidden-text-strip#205

Merged
twschiller merged 1 commit into
mainfrom
fix/hidden-text-strip-modern-color-syntaxes
Jun 6, 2026
Merged

Fix: resolve modern CSS color syntaxes in hidden-text-strip#205
twschiller merged 1 commit into
mainfrom
fix/hidden-text-strip-modern-color-syntaxes

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

Summary

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

hidden-text-strip's parseColor was regex-only and accepted just rgb() / rgba(). Tailwind v4, Open Props, and any stylesheet authored against CSS Color Level 4 emit oklch(), lab(), color(), and color-mix() in computed style verbatim, so:

  • An element with color: oklch(...) matching its background wasn't caught by detectColorMatchparseColor returned null on the modern syntax and the candidate was preserved.
  • An element with a modern-syntax ancestor background caused effectiveBackgroundColor to bail (the existing intentional behavior, added to avoid stripping bg-orange-500 buttons with white text inside a white card).

The result: a white-on-white injection painted via color: oklch(0.99 0 0) on a plain white card was invisible to humans and invisible to the rule.

Approach

Add a 1×1 canvas probe as a fallback after the regex parser fails. The browser's 2D context accepts any valid CSS color string and silently ignores anything else.

  • Two-sentinel probe (#1a2b3c then #fedcba) reliably distinguishes a parsed value (both probes converge to the same resolved fillStyle) from a rejected one (each probe keeps its own sentinel).
  • Once accepted, paint and getImageData to recover the resolved RGBA including alpha.
  • Per-realm singleton canvas — lazy-init at first call, persisted for the lifetime of the realm. No alloc on hot paths.

The existing regex parser runs first, so the common rgb() / rgba() path stays a single regex match.

Test plan

  • bun run test -- hidden-text-strip — all 41 cases green (36 existing + 5 new).
  • bun run preflight — codegen + Biome + ESLint + typecheck + knip + Jest all green.
  • New tests cover:
    • White-on-white via color: oklch(...) is correctly stripped.
    • Near-black-on-near-black via lab(...) is correctly stripped.
    • color-mix() matching the ancestor background is correctly stripped.
    • The existing oklch-orange-bg-with-white-text case is preserved (now for the right reason — perceptual distance is huge, not because we bailed).
    • An unparseable value still returns null (rule preserves the element).

Notes for reviewers

  • jsdom test environment: getContext('2d') returns null without the canvas package, so the canvas path silently no-ops in the existing test suite — the regex parser fully handles those cases. The new tests install a deterministic stub canvas that mimics the spec's silent-ignore behavior; the stub also wires the two sentinels through so the rule's probe round-trips correctly. __resetColorProbeForTesting is exported solely so the test can re-prime the cached canvas after installing the stub.
  • Why not canvas-first? The regex parser is faster than allocating + probing canvas for the dominant rgb() / rgba() shape that browsers emit by default, and keeps all existing tests exercising the same code path they do today.
  • Reading back alpha: the canonical fillStyle read-back for the #rrggbb form drops alpha; sampling via getImageData keeps it.

🤖 Generated with Claude Code

Tailwind v4, Open Props, and any stylesheet authored against CSS Color
Level 4 emit `oklch()`, `lab()`, `color()`, and `color-mix()` in
computed style verbatim. hidden-text-strip's `parseColor` was
regex-only and accepted just `rgb()` / `rgba()`, so any element with a
modern-syntax `color` slipped past `detectColorMatch` and any modern
ancestor background made `effectiveBackgroundColor` bail.

A white-on-white injection painted via `color: oklch(0.99 0 0)` on a
plain white card was invisible to humans and invisible to the rule.

Add a 1×1 canvas probe as a fallback after the regex parser fails. The
browser's 2D context accepts any valid CSS color string and silently
ignores anything else — the two-sentinel probe (`#1a2b3c` then
`#fedcba`) reliably distinguishes a parsed value (both probes converge
to the same resolved fillStyle) from a rejected one (each probe keeps
its own sentinel). Once accepted, paint and `getImageData` to recover
the resolved RGBA including alpha.

Per-realm singleton canvas so we don't allocate on every parse. jsdom
returns null from `getContext('2d')` without the `canvas` package, so
the existing test path keeps running through the regex parser. New
tests install a deterministic stub canvas that mimics the spec's
silent-ignore behavior, exercising the rule against oklch / lab /
color-mix syntaxes and confirming that an unparseable value still
returns null.
@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 1:38pm

Request Review

@twschiller twschiller merged commit 3ab2c5b into main Jun 6, 2026
7 checks passed
@twschiller twschiller deleted the fix/hidden-text-strip-modern-color-syntaxes 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