shadow-dom.md (5820B)
1 --- 2 title: "Shadow DOM" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/xss-cross-site-scripting/shadow-dom.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/shadow-dom.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Shadow DOM 14 15 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.<sup>[[3]](#references)</sup> 16 17 ## Why attackers care 18 19 - `mode: "closed"` only hides the root from `element.shadowRoot`; code running in the same JS realm can still intercept its creation.<sup>[[1]](#references)</sup> 20 - DSD allows HTML such as `<template shadowrootmode="open">...</template>` to create real shadow roots during parsing. 21 - Newer sinks such as `Element.setHTMLUnsafe()` and `ShadowRoot.setHTMLUnsafe()` can materialize DSD from attacker-controlled HTML. 22 - Some sanitizers still treat `<template>`/DSD as inert markup and become bypassable when they return **live DOM** instead of a serialized string. 23 24 ## Enumerating shadow roots 25 26 Open roots are trivial to enumerate: 27 28 ```javascript 29 for (const el of document.querySelectorAll('*')) { 30 if (el.shadowRoot) console.log('shadow host:', el, el.shadowRoot); 31 } 32 ``` 33 34 When testing large applications, recursively walk every discovered `shadowRoot` because Web Components are often nested several levels deep. 35 36 ## Intercepting `closed` roots 37 38 If you can run JavaScript **before** the component is initialized, hook `attachShadow()` and keep the returned reference: 39 40 ```javascript 41 (() => { 42 const orig = Element.prototype.attachShadow; 43 window.__shadowRoots = []; 44 Element.prototype.attachShadow = function (init) { 45 const root = orig.call(this, init); 46 window.__shadowRoots.push({ host: this, mode: init.mode, root }); 47 return root; 48 }; 49 })(); 50 ``` 51 52 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. 53 54 ## Declarative Shadow DOM as an injection surface 55 56 DSD creates a shadow root directly from markup: 57 58 ```html 59 <div id="host"> 60 <template shadowrootmode="open"> 61 <img src=x onerror=alert(document.domain)> 62 </template> 63 </div> 64 ``` 65 66 Important parsing behavior for exploitation:<sup>[[2]](#references)[[4]](#references)</sup> 67 68 - `innerHTML` **does not** create declarative shadow roots. 69 - `Element.setHTMLUnsafe()` / `ShadowRoot.setHTMLUnsafe()` **do** parse them. 70 - Server-rendered HTML also creates DSD during the normal page parse. 71 72 Therefore, look for applications that: 73 74 - hydrate server-rendered components, 75 - import remote HTML snippets into custom elements, 76 - expose preview/render features backed by `setHTMLUnsafe()`, or 77 - wrap parsing APIs in custom “safe HTML” helpers. 78 79 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. 80 81 A useful gadget is `slotchange`: 82 83 ```html 84 x<template shadowrootmode=open><slot onslotchange=alert(1)> 85 ``` 86 87 The leading text node (`x`) becomes assigned content for the default slot, which fires `slotchange`. 88 89 ## Sanitizer and mXSS footguns 90 91 Recent sanitizer bypasses made DSD more relevant for XSS research: 92 93 - 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. 94 - Returning a string is often safer here because `innerHTML` serialization does **not** include shadow roots. 95 - Allowing `<template>` without recursively sanitizing its contents, or allowing the `shadowrootmode` attribute, can convert apparently inert markup into executable DOM. 96 - Template handling is also a good place to look for **mutation XSS** bugs when sanitized markup is later re-parsed in a different context. 97 98 Typical risky pattern: 99 100 ```javascript 101 const frag = sanitize(userHTML, { returnDOM: true }); 102 target.appendChild(frag); 103 ``` 104 105 If `frag` still contains a declarative shadow root, the append operation can reintroduce attacker-controlled handlers inside the page. 106 107 ## Dumping serializable / clonable shadow roots 108 109 Newer Shadow DOM features are useful post-XSS recon primitives: 110 111 - `shadowrootserializable` / `attachShadow({ serializable: true })` 112 - `shadowrootclonable` / `attachShadow({ clonable: true })` 113 114 Examples: 115 116 ```javascript 117 host.getHTML({ serializableShadowRoots: true }); 118 const copy = host.cloneNode(true); 119 ``` 120 121 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. 122 123 ## Practice 124 125 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.<sup>[[5]](#references)</sup> 126 127 ## References 128 129 - [1] [The Closed Shadow DOM](https://blog.ankursundara.com/shadow-dom/) 130 - [2] [Declarative Shadow DOM explainer](https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md) 131 - [3] [MDN — Using shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) 132 - [4] [MDN — `Element.setHTMLUnsafe()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTMLUnsafe) 133 - [5] [DiceCTF `shadow` challenge writeup](https://github.com/Super-Guesser/ctf/blob/master/2022/dicectf/shadow.md)