postMessage XSS lab
Scenarios
Every row is one scenario, and every scenario is two pages: the page of the
application under test, and the page you drive it from. The two sit on
different origins on purpose — a postMessage
handler that trusts "*" (or a sloppy origin check) has to
be reachable from somewhere else for the exercise to mean anything.
Every participant runs on its own real origin — a separate hostname under
The attacker page takes the payload from its own URL (
pmlab.amir.baby, all served by this one server. Origin checks are genuine and a blocked message really is blocked.
The attacker page takes the payload from its own URL (
?payload=…), so a whole scenario can be run from one
crafted link — send it through Burp, or just open it.
| # | Finding | Application under test | Drive it from |
|---|---|---|---|
| S1 | Wildcard target origin The listener accepts every sender and writes the payload into the DOM as HTML. | https://victim.pmlab.amir.baby/s1 | https://attacker.pmlab.amir.baby/s1 |
| S2 | Weak domain validation The origin check looks for the trusted domain inside event.origin, so a subdomain of it passes. | https://victim.pmlab.amir.baby/s2 | https://attacker.trusted-domain.pmlab.amir.baby/s2 |
| S3 | Weak regex in the origin check The origin is matched with a regular expression whose .* swallows extra labels. | https://victim.pmlab.amir.baby/s3 | https://attacker.com.trusted-origin.pmlab.amir.baby/s3 |
| S4 | Data exposure with a wildcard target origin The session context is sent with "*" as the target origin, so whoever opened or framed the page receives it. | https://victim.pmlab.amir.baby/s4 | https://attacker.pmlab.amir.baby/s4 |
| S5 | The hardened listener The same flow with an origin allowlist, a source check and a text-only sink. | https://victim.pmlab.amir.baby/s5 | https://attacker.pmlab.amir.baby/s5 |
How the flow works
Sender Window Receiver Window
────────────── ───────────────
const receiverURL = "http://localhost/receiver.html";
const childWindow = window.open(receiverURL); window.addEventListener('message', (event) => {
document.querySelector('p').textContent = event.data;
function sendMessage() { });
const message = "Hello, Receiver!";
childWindow.postMessage(message, "*");
The receiver decides how much it trusts the sender. Everything in this lab is a variation of that one line — what the receiver checks, and what it does with the payload when it believes the sender.