Shadow DOM
Shadow DOM encapsulates DOM and CSS implementation details, but it is not a security boundary for script already executing in the same origin/realm. It creates useful assessment surfaces when applications mix closed roots, Declarative Shadow DOM (DSD), and HTML parsing/sanitizing APIs.[3]
Why attackers care
mode: "closed"only hides the root fromelement.shadowRoot; code running in the same JS realm can still intercept its creation.[1]- DSD allows HTML such as
<template shadowrootmode="open">...</template>to create real shadow roots during parsing. - Newer sinks such as
Element.setHTMLUnsafe()andShadowRoot.setHTMLUnsafe()can materialize DSD from attacker-controlled HTML. - Some sanitizers still treat
<template>/DSD as inert markup and become bypassable when they return live DOM instead of a serialized string.
Enumerating shadow roots
Open roots are trivial to enumerate:
for (const el of document.querySelectorAll('*')) {
if (el.shadowRoot) console.log('shadow host:', el, el.shadowRoot);
}
When testing large applications, recursively walk every discovered shadowRoot because Web Components are often nested several levels deep.
Intercepting closed roots
If you can run JavaScript before the component is initialized, hook attachShadow() and keep the returned reference:
(() => {
const orig = Element.prototype.attachShadow;
window.__shadowRoots = [];
Element.prototype.attachShadow = function (init) {
const root = orig.call(this, init);
window.__shadowRoots.push({ host: this, mode: init.mode, root });
return root;
};
})();
This is one of the most important takeaways when auditing apps or browser extensions that wrongly assume closed protects secrets, CSRF tokens, anti-clickjacking UI, or privileged controls.
Declarative Shadow DOM as an injection surface
DSD creates a shadow root directly from markup:
<div id="host">
<template shadowrootmode="open">
<img src=x onerror=alert(document.domain)>
</template>
</div>
Important parsing behavior for exploitation:[2][4]
innerHTMLdoes not create declarative shadow roots.Element.setHTMLUnsafe()/ShadowRoot.setHTMLUnsafe()do parse them.- Server-rendered HTML also creates DSD during the normal page parse.
Therefore, look for applications that:
- hydrate server-rendered components,
- import remote HTML snippets into custom elements,
- expose preview/render features backed by
setHTMLUnsafe(), or - wrap parsing APIs in custom “safe HTML” helpers.
Also remember that classic <script> tags inserted from HTML strings stay inert, so prefer event handlers, javascript: URLs, or event gadgets inside the shadow tree.
A useful gadget is slotchange:
x<template shadowrootmode=open><slot onslotchange=alert(1)>
The leading text node (x) becomes assigned content for the default slot, which fires slotchange.
Sanitizer and mXSS footguns
Recent sanitizer bypasses made DSD more relevant for XSS research:
- If a sanitizer parses attacker HTML into a DOM tree and returns live DOM (for example
DocumentFragment) instead of a string, a hidden DSD subtree may survive the cleanup step. - Returning a string is often safer here because
innerHTMLserialization does not include shadow roots. - Allowing
<template>without recursively sanitizing its contents, or allowing theshadowrootmodeattribute, can convert apparently inert markup into executable DOM. - Template handling is also a good place to look for mutation XSS bugs when sanitized markup is later re-parsed in a different context.
Typical risky pattern:
const frag = sanitize(userHTML, { returnDOM: true });
target.appendChild(frag);
If frag still contains a declarative shadow root, the append operation can reintroduce attacker-controlled handlers inside the page.
Dumping serializable / clonable shadow roots
Newer Shadow DOM features are useful post-XSS recon primitives:
shadowrootserializable/attachShadow({ serializable: true })shadowrootclonable/attachShadow({ clonable: true })
Examples:
host.getHTML({ serializableShadowRoots: true });
const copy = host.cloneNode(true);
If the application opted in, this can expose shadow DOM content that would otherwise stay out of normal innerHTML output. This is especially interesting when developers use Shadow DOM to hide tokens, comments, feature flags, or internal UI state.
Practice
A good lab for this topic is the DiceCTF shadow challenge, which chained DOM XSS with closed-shadow-root abuse and CSS-based extraction ideas.[5]