Fix: cover open declarative shadow DOM via setHTMLUnsafe (#203)#211
Merged
Conversation
Addresses audit item #4 (open variant). `<template shadowrootmode="open">` materialized post-parse via `Element.setHTMLUnsafe` or `ShadowRoot.setHTMLUnsafe` bypassed both the `attachShadow` patch and the subtree-watcher's "host was just inserted" path — the receiver is already in the tree and only its descendants mutate, so a host that gains a shadow this way was invisible to every shadow-piercing rule. The extension now wraps both `setHTMLUnsafe` variants in `installShadowRootHook` and walks the receiver afterward so any new open DSD shadow lands in the registry and fans out to subscribers. Closed DSD inherits the same opt-out contract as imperative closed shadows; the main-world probe sketched in `closed-shadow-root-annotate` is captured as future work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
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
Addresses red-team audit item #4 (open variant) from #203.
<template shadowrootmode=\"open\">materialized post-parse viaElement.setHTMLUnsafeorShadowRoot.setHTMLUnsafewas a blind spot inthe shadow-piercing plumbing:
attachShadow, so theattachShadowpatch ininstallShadowRootHookdoesn't fire.
was just inserted" path doesn't fire either — only the receiver's
descendants are added in the MutationRecord, but the shadow itself is
attached to the receiver, not to any added node.
This left a clean carrier shape for SSR-style component frameworks (Lit,
Stencil, RSC experiments) that hydrate via
setHTMLUnsafe: the samecontent carried via imperative
attachShadowwas already covered, so theDSD path was a "switch to the new API to bypass the rules" shortcut.
installShadowRootHooknow also wrapsElement.prototype.setHTMLUnsafeand
ShadowRoot.prototype.setHTMLUnsafe. Each trampoline calls throughto the original and then walks the receiver with the existing
discoverShadowRootsIn, which registers any new open shadow and fans itout to subscribers (the subtree-watcher's
subscribeShadowRootAttachedcallback handles routing on host containment). Initial-parse DSD was
already covered by the startup walk; this PR closes the post-parse gap.
Closed DSD inherits the same opt-out contract as imperative closed
shadows. A main-world probe over
Element.prototype.attachShadow(samedelivery pattern as
webdriver-probe-annotate) is the long-term pathand is captured in the
closed-shadow-root-annotateheader as futurework.
Notable choices
Document.parseHTMLUnsafenot patched — it returns a detachedDocument; any path that grafts its content into the live tree goesthrough
appendChild(caught by the subtree-watcher's discovery walk)or another
setHTMLUnsafecall (caught here).if the method is absent on the prototype, the patch is a no-op and the
imperative-only coverage remains.
registerShadowRootguards on the Set membership, so subscribers fire exactly once per
open root even when both the trampoline and (in tests) the polyfilled
attachShadowpath see the same root.jest-environment-jsdom@30ships jsdom 26, whichhasn't shipped
setHTMLUnsafe.src/__test-mocks__/jsdom-extras.tsgains a feature-gated polyfill that emulates the parser's DSD lifting:
top-level
<template shadowrootmode>→ shadow on receiver,descendant template → shadow on its parent element, child-first
recursion for nested DSD. The shim is gated on the absence of the
native method; a future jsdom that ships DSD natively will use the
engine implementation.
ShadowDomEmbed.tsxgains a third host thathydrates via
Element.setHTMLUnsafewith a<template shadowrootmode=\"open\">payload carrying the existingPRODUCT_DETAIL_HIDDEN_SYSTEMinjection fixture. Reviewers can seethe rule pipeline reaching into a DSD shadow in the before/after view.
the three open-shadow attachment paths covered (imperative, parse-time
DSD, post-parse
setHTMLUnsafe) and explicitly notes that closedshadows remain unreached regardless of attachment path. The
closed-shadow-root-annotatedoc section's DSD clause clarifies thatthe open variant is now covered.
Test plan
npx jest src/lib/__tests__/shadow-roots*— 22 tests (16 handwritten + 6 property cases), 6 new for DSD: open-on-host registers,
closed-on-host does not, nested-host DSD registers, ShadowRoot
setHTMLUnsafe registers descendants, listener fires exactly once
per root, repeated
setHTMLUnsafedoesn't double-register.npx jest src/lib/__tests__/shadow-roots.property.test.ts— 1 newproperty: for any random nested tree of open/closed DSD templates,
the registry equals exactly the set of open shadows.
npx jest src/lib/__tests__/subtree-watcher.test.ts— 72 tests,1 new integration confirming a subscriber receives a shadow-side
element via the setHTMLUnsafe path with no other intervention.
npx jest— full extension suite, 1726 tests pass.bun run check— biome + eslint clean.bun run typecheck— clean.bun run knip— clean.bun run checkindemo-site/— biome + eslint clean.pre-commit run --files docs/src/content/docs/rules.md— mdformat+ markdownlint clean.
card's injection paragraph is hidden behind a reveal placeholder
with shadow-piercing rules enabled (and that the closed-shadow card
continues to surface the
closed-shadow-root-annotatelandmark).🤖 Generated with Claude Code