daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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)