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

Skip to content

Feat: shadow-piercing text walkers (#164 Tier 2)#166

Merged
twschiller merged 1 commit into
mainfrom
feat/shadow-pierce-text-walkers
Jun 5, 2026
Merged

Feat: shadow-piercing text walkers (#164 Tier 2)#166
twschiller merged 1 commit into
mainfrom
feat/shadow-pierce-text-walkers

Conversation

@twschiller

@twschiller twschiller commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Tier 1 (Feat: shadow-aware subtree watcher (#164 Tier 1) #165) made the subtree watcher and selector dispatch reach into open shadow roots. Tier 2 does the same for the text-walk path that the injection-defense rules depend on: prompt-injection-redact, pii-redact, secrets-redact, encoded-payload-redact, unicode-invisibles-strip.
  • Without this, an attacker could plant an injection payload inside a <div>.attachShadow({mode:"open"}) and every one of these rules would walk right past it — the entire point the rules exist (asymmetry between what the user sees and what an agent reads) was bypassable with one line of page JS.

Part of #164 Tier 2.

What changed

  • extension/src/lib/dom-utils.ts
    • New collectTextNodesShadowPiercing shared collector — recursive walk that descends through element.shadowRoot for open hosts.
    • walkTextNodes now delegates to it (same public contract; same filter; just sees shadow content too).
    • visibleTextContent rewritten as a recursive concat that descends into open shadows; closed shadows stay opaque.
  • extension/src/lib/yielding-text-walk.ts — the chunked walker now uses the shared collector for phase 1 and iterates by index in phase 2. Drops the walker.currentNode resume dance the original needed because the consumer detaches nodes mid-chunk; pre-collection makes that problem disappear (the array holds direct refs) and unlocks shadow piercing in one move. Chunking + scheduler-yield + abort semantics are unchanged.
  • extension/src/lib/__tests__/text-walk-shadow.property.test.ts (new) — fast-check property tests for:
    • Coverage: walkTextNodes returns exactly the text nodes reachable through light + open-shadow boundaries for any random tree.
    • Eager / chunked equivalence: union of walkTextNodesChunked chunks equals walkTextNodes across nested shadows, in both sync and async paths.
    • visibleTextContent concatenates the same reachable set in pre-order across nested shadows.
    • Closed-shadow exclusion: text the builder places into a closed shadow root never surfaces (non-vacuous — the builder captures the shadow root at attach time so it can actually place content there).
  • demo-site/src/components/ShadowDomEmbed.tsx — adds an injection-shaped paragraph inside the existing demo shadow root. Fixture reused from product detail (already base64 in source — no new plaintext payload shipped).

Closed shadow roots stay opaque by design. Tier 3 (adoptedStyleSheets for EasyList + placeholder styles inside shadows) is the remaining piece tracked in #164.

Test plan

  • bun jest — 1322 / 1322 pass (6 new property tests; existing yielding-text-walk.property.test.ts equivalence tests continue to pass unchanged across the rewrite).
  • bun run check — biome + eslint clean (extension + demo-site).
  • bun run build — both bundles build clean.
  • Manual: load the unpacked extension, open the demo home page, confirm the prompt-injection paragraph inside the shadow root is replaced with a reveal placeholder.

🤖 Generated with Claude Code

Tier 1 (#165) made the subtree watcher and selector dispatch reach
into open shadow roots. Tier 2 does the same for the text-walk path
the injection-defense rules depend on:

- prompt-injection-redact
- pii-redact
- secrets-redact
- encoded-payload-redact
- unicode-invisibles-strip

The walkers all bottomed out in `document.createTreeWalker`, which
stops at shadow boundaries. An attacker could plant an injection
payload inside a `<div>.attachShadow({mode:"open"})` and every one
of these rules would walk right past it.

Approach: replace the TreeWalker in `yielding-text-walk.ts` and in
`walkTextNodes` / `visibleTextContent` (dom-utils) with a shared
recursive collector that descends through `element.shadowRoot` for
open hosts. Closed shadows stay opaque by design.

Pre-collection instead of an interleaved TreeWalker also drops the
walker.currentNode resume dance the chunked walker had to maintain
across consumer detachment — the array holds direct refs.

Demo: extends the existing ShadowDomEmbed on the home page with a
prompt-injection paragraph (reused fixture from product detail,
already base64 in source) so reviewers can confirm the text walker
reaches into the shadow and the placeholder lands.

Property tests for three structural invariants:

  - walkTextNodes / collectTextNodesShadowPiercing return exactly
    the text nodes reachable through light + open-shadow boundaries
    for any random tree, with parent filtering applied.
  - visibleTextContent concatenates the same reachable set in
    pre-order across nested shadows.
  - The chunked walker's union of processed chunks matches the
    eager walker — equivalence holds across the shadow extension
    in both sync and async paths.

Plus a non-vacuous exclusion property: text living inside a closed
shadow root never surfaces in any walker output, even when the
builder explicitly places content there. The earlier hand-written
tests for chunked-walker equivalence (in
yielding-text-walk.property.test.ts) continue to pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vercel

vercel Bot commented Jun 5, 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 5, 2026 3:26pm

Request Review

@twschiller twschiller merged commit 8fec393 into main Jun 5, 2026
7 checks passed
@twschiller twschiller deleted the feat/shadow-pierce-text-walkers branch June 5, 2026 15:28
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