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

Skip to content

Fix: main-world shadow-root probe for definitive closed-shadow detection (#203)#217

Merged
twschiller merged 1 commit into
mainfrom
shadow-root-main-world-probe
Jun 7, 2026
Merged

Fix: main-world shadow-root probe for definitive closed-shadow detection (#203)#217
twschiller merged 1 commit into
mainfrom
shadow-root-main-world-probe

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

Summary

Addresses red-team audit item #1 (closed shadow root content invisible) and the open-shadow blind spot called out in PR #211's footnote, both from #203.

Wraps Element.prototype.attachShadow, Element.prototype.setHTMLUnsafe, and ShadowRoot.prototype.setHTMLUnsafe in the page world via the same delivery pattern as webdriver-probe-annotate and checkout-checkbox-sanitize. The isolated-world copies of these prototypes are distinct objects from the page-world copies, so the existing patches in lib/shadow-roots.ts only fired for isolated-world callers and jsdom-shimmed test paths — page-script attachments slipped past entirely.

Two events bridge across worlds:

  • abs:closed-shadow-attached → consumed by closed-shadow-root-annotate for a definitive landmark stamp. Supersedes the structural heuristic — no canvas/WebGL false positives, catches closed shadows on non-custom-element hosts. Carries no detail so closed shadow contents remain opaque to every consumer.
  • abs:shadow-discover with detail.target → consumed by shadow-roots.ts, routes the receiver through discoverShadowRootsIn so page-script open-shadow attachments and setHTMLUnsafe receivers are no longer invisible to the isolated-world rule engine.

The structural heuristic stays as the gap-filler for the active tab between toggle-on and the moment the executeScript fallback injects the probe.

Notable choices

  • Gated on closed-shadow-root-annotate — the registration lifecycle (lib/shadow-root-probe-registration.ts) mirrors webdriver-probe-registration.ts: enrolled when (enforcement on) AND (rule enabled). Keeps the page-world surface gated to opt-in users.
  • allFrames: true — shadow roots attach in any frame and same-origin iframes have their own prototype copies, so per-frame injection is required.
  • No detail on closed events — preserves the spec's encapsulation guarantee. A parity test asserts event.detail === null so a future "convenient payload" tweak can't silently leak host or root references.
  • Forged-dispatch defense — the isolated-world abs:shadow-discover listener gates on target instanceof Node so a page firing abs:shadow-discover with garbage detail no-ops cleanly.
  • Closed DSD still uncovered<template shadowrootmode=\"closed\"> is consumed by the HTML parser without going through attachShadow, so the probe doesn't see it. The materialized root is also indistinguishable from "no shadow" externally, so the heuristic can't catch it reliably either. Documented in the rule's header.
  • Docs reflect current behavior onlydocs/src/content/docs/rules.md rewrites the closed-shadow-root-annotate section around the new probe-primary / heuristic-fallback split. The coverage-scope section is unchanged (open-shadow story is materially the same — the probe is belt-and-suspenders for open).
  • Test isolation note — the new registration test prefixes its identifiers (shadowProbeRegisterMock, ShadowProbeRegistrationModule, etc.) so they don't collide with the identical shapes in webdriver-probe-registration.test.ts. Both files have no top-level import/export and TypeScript treats them as scripts that share global scope; Biome's noExportsInTest rule blocks the export {} workaround.

Test plan

  • npx jest src/lib/__tests__/shadow-root-probe-source.test.ts — 8 tests covering attachShadow open/closed dispatch, setHTMLUnsafe receiver dispatch, idempotency, no-detail-on-closed contract.
  • npx jest src/lib/__tests__/shadow-root-probe-source.property.test.ts — 2 property invariants: event count equals op count across random attachShadow/setHTMLUnsafe sequences; discover-event target equals the receiver.
  • npx jest src/lib/__tests__/shadow-root-probe-registration.test.ts — 5 tests covering the four enabled/registered corners plus the no-op short-circuit.
  • npx jest src/lib/__tests__/shadow-roots.test.ts — 3 new tests on the abs:shadow-discover listener: open-root registration from probe dispatch, ShadowRoot-target walking, forged-dispatch defense.
  • npx jest src/rules/__tests__/closed-shadow-root-annotate.test.ts — 5 new tests on the probe integration: landmark stamped from event alone (no heuristic match), inject-shadow-root-probe requested on apply, per-document dedupe on repeated events, listener removed on teardown, rule-detection emitted exactly once.
  • npx jest — full extension suite, 93 suites / 1856 tests pass.
  • bun run check — biome + eslint clean.
  • bun run typecheck — clean.
  • bun run knip — clean (entry added for the new bundle).
  • bun run build — extension builds; dist/shadow-root-probe.js ships as its own bundle.
  • pre-commit run --files docs/src/content/docs/rules.md — mdformat + markdownlint pass.

🤖 Generated with Claude Code

…ion (#203)

Addresses red-team audit items #1 (closed shadow root content invisible)
and the open-shadow blind spot identified in PR #211's footnote, both
from #203.

Wraps Element.prototype.attachShadow, Element.prototype.setHTMLUnsafe,
and ShadowRoot.prototype.setHTMLUnsafe in the page world via the same
delivery pattern as webdriver-probe-annotate and
checkout-checkbox-sanitize. The isolated-world copies of these
prototypes are distinct objects from the page-world copies, so the
existing patches in lib/shadow-roots.ts never fired on page-script
attachments. Two events bridge back across worlds:

- abs:closed-shadow-attached → consumed by closed-shadow-root-annotate
  for a definitive landmark stamp. Supersedes the structural heuristic
  (no canvas/WebGL false positives, catches closed shadows on
  non-custom-element hosts). Carries no detail — closed shadow
  contents must remain opaque to every consumer.
- abs:shadow-discover with detail.target → consumed by shadow-roots.ts,
  routes the receiver through discoverShadowRootsIn so page-script
  open-shadow attachments and setHTMLUnsafe receivers are no longer
  invisible to the isolated-world rule engine.

The structural heuristic stays as the gap-filler for the active tab
between toggle-on and the moment the executeScript fallback injects
the probe.

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

vercel Bot commented Jun 7, 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 7, 2026 9:54pm

Request Review

@twschiller twschiller merged commit ef40661 into main Jun 7, 2026
7 checks passed
@twschiller twschiller deleted the shadow-root-main-world-probe branch June 7, 2026 21:58
twschiller added a commit that referenced this pull request Jun 7, 2026
Three main-world bundle registration tests (webdriver-probe,
checkout-checkbox-defense, shadow-root-probe) had identical ~50-line
blocks wiring chrome.scripting.registerContentScripts /
unregisterContentScripts / getRegisteredContentScripts into a stateful
in-memory store. Each new bundle would copy-paste the same mock — the
canonical rule-of-three trigger.

Extracted to src/__test-mocks__/chrome-scripting-registry.ts as
installScriptingRegistry(), exposing { registerMock, unregisterMock,
getRegisteredMock, seed, registered }. The per-test variance (which
module to import, which startup function to call, which script literal
to seed) stays local.

Net: 251 lines removed, 130 added across the three tests + the shared
helper. The seed/registered abstraction also drops the inline
RegistrationModule prefix-collision workaround in
shadow-root-probe-registration.test.ts (the colliding identifiers moved
into the helper module).

Stacked on shadow-root-main-world-probe (PR #217).

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