DOMinator is now live on the Chrome Web Store. It’s free, open source under the MIT license (github.com/vinothsparrow/DOMinator), and it runs entirely in your browser: no account, no server, no analytics.

Why I built it
Back in 2020 I wrote about a $20000 Facebook DOM XSS found through a postMessage handler. To find that bug I wrote a small extension called iframe-broker, which logged the cross-window messages on a page so I didn’t have to set breakpoints by hand.
iframe-broker answered one question: what messages are flying around? Finding a real bug takes a lot more than that:
- Which listener actually received this message, and where was it registered?
- Does the handler check
event.origin? Can that check be bypassed? - Does anything from
event.dataend up ininnerHTML,evalorlocation? - How do I send a modified message as if it came from another origin?
- How do I turn all of that into a PoC I can put in a report?
I used to answer each of those by hand with DevTools, the Sources panel, some console snippets and a lot of patience. DOMinator does all of it in one place.
What it captures
Once installed, DOMinator hooks every frame at document_start, before the page’s own scripts run, so it sees the messages sent and the listeners registered during page load. That early window is often where the interesting init payloads are.
Messages. Every window.postMessage call and every message event is recorded with its direction, its origin and target, its frame, and its payload type and size. It covers more than window: MessagePort, BroadcastChannel, Worker/SharedWorker and ServiceWorker too. Each send records its call site (file:line:column) and full stack. In DevTools, clicking the call site opens the file in the Sources panel, source-mapped when the map is available. Each receive is paired with the send that produced it, so you can see who sent it without guessing.
Listeners. For every addEventListener('message', …) or onmessage handler, DOMinator shows where it was registered and what kind of origin check it does. It doesn’t just say whether a check exists:
| Kind | What it means |
|---|---|
none | No origin check at all |
strict | === comparison or an allowlist |
bypassable | startsWith, endsWith, indexOf, includes or an unanchored regex |
source | Compares event.source to parent/opener and never checks the origin |
The bypassable bucket is where the bugs usually are. event.origin.indexOf("example.com") > -1 looks like a check in code review, but example.com.evil.com passes it.
It also lists the dangerous sinks in the handler body (innerHTML, document.write, eval, location, srcdoc, jQuery html(), storage and cookie writes, DOMParser, import() and more). One thing that always bothered me in DevTools is that a listener shows up as some error-monitoring or jQuery wrapper instead of the real code. DOMinator unwraps the common ones, so you see the actual handler.
Findings. Separate from postMessage, the Findings tab picks up prototype pollution (writes onto Object.prototype through Object.assign, jQuery.extend and lodash merge), DOM clobbering (id/name collisions that overwrite globals like getElementById), and URL/DOM taint sources like location.hash, document.referrer and window.name.

From suspicion to confirmed flow
“This handler has innerHTML in it” is only a hint. What you want to know is whether your value actually reaches the innerHTML.
DOMinator’s dynamic sink trace follows taint through the transforms real apps apply: JSON.parse/stringify, encodeURIComponent/decodeURI, btoa/atob, replace, split/join, case changes and so on. Runtime hooks on the sinks record when a tainted value lands in one. If Trusted Types or CSP blocks the assignment, the flow is still recorded, marked blocked, which is useful to know when you look for a way around it.
When a flow is confirmed, the message gets a red confirmed badge and shows up in the Findings tab, pointing at the exact file:line of the sink.
Active testing
DOMinator doesn’t just record. These features turn a capture into a test:
- Replay. Every captured message has a Replay button that opens an editor with the payload already filled in. For nested JSON it’s a collapsible tree where you can inject into any field (XSS, canary or
__proto__). You pick the target frame, thetargetOrigin, and the origin shown to listeners: the real page origin, orevil.com,evil.TARGET,TARGET.evil.comor a custom one. That’s how you show astartsWithorendsWithcheck is bypassable without setting up a domain first. - Intercept. Pause a message inside the listener, change the origin and payload, then deliver it or drop it. It’s useful for one-shot init messages that are gone before you can react. It auto-delivers after 20 seconds so the page doesn’t hang.
- Auto-probe and canary fuzzing. When turned on, each new listener gets a unique canary string, a nested JSON probe and a
__proto__payload, and any confirmed flows show up in the message list. - Auto-generated PoC. Select a listener and DOMinator writes a proof of concept: an iframe page, a
window.open/opener page, or a console/Burp snippet. It uses a JSON field that actually reached the listener and picks the payload based on the sink. You can copy it or download it and serve it.
Replay, Intercept and Auto-probe are off by default, and they only act on the page open in DevTools.
Using it in automation
The page exposes a small helper, so DOMinator also works in headless runs:
// In Playwright, with the extension loaded
await page.goto(url, { waitUntil: "networkidle" });
const result = await page.evaluate(() => window.__DOMINATOR__.dump());
// { messages, listeners, flows }
Run that across a list of targets and you have a first pass that flags pages with none or bypassable listeners and confirmed flows before you look at anything by hand.
Limitations
A send to a cross-origin window (iframe.contentWindow.postMessage(…) or parent.postMessage(…) across origins) goes through the browser’s cross-origin proxy, and no page script can hook that. So those sends have no call site. The message is still captured in the receiving frame, with its origin and the listeners it reaches.
Origin spoofing in Replay is a presented origin: a synthetic MessageEvent or the value the wrapped listener sees. It can’t change the origin the browser enforces on a real postMessage. That’s enough to prove a weak check. For the final report you still host the PoC on a real attacker origin.
Get it
- Chrome Web Store: DOMinator
- Source: github.com/vinothsparrow/DOMinator (a Firefox build is available from source with
yarn build:firefox) - Changelog: GitHub releases
Open DevTools, select the DOMinator panel and reload the page. Any page that embeds a video player, payment widget or chat widget will give you traffic to look at right away.
Replay and probing send payloads into the page you’re testing, so please use DOMinator only on sites you own or are authorised to test.
Issues and PRs are welcome. If DOMinator helps you find a bug, I’d love to hear about it on X.