postMessage XSS lab
Acme Portal
This is the application under test. It embeds third-party widgets in iframes and
talks to them with postMessage. Every scenario below
is the flow from
“postMessage XSS” (Arash Shahbazi, Jan 2025),
reproduced exactly — including the parts that need correcting.
How the lab is wired. Every participant runs on its own real origin — a separate hostname under
pmlab.amir.baby, all served by this one server. Origin checks are genuine and a blocked message really is blocked.
Open the victim page and the attacker page in two tabs — they must be
different origins for the attack to mean anything.
Scenarios
| # | Writeup finding | Victim page | Attacker page |
|---|---|---|---|
| S1 | Insecure listener + postMessage(data, "*") → XSS in the receiver |
/s1-widget | https://attacker.pmlab.amir.baby/s1 |
| S2 | Weak domain validation → a subdomain of the trusted domain is accepted | /s2-base (weak) /s2-exact (as written in the writeup) |
https://attacker.trusted-domain.pmlab.amir.baby/s2 |
| S3 | Weak regex in the origin check → bypassed by a crafted subdomain | /s3-regex | https://attacker.com.trusted-origin.pmlab.amir.baby/s3 |
| S4 | Data exposure — sensitive data sent with "*" is sniffed |
/s4-session | https://attacker.pmlab.amir.baby/s4 |
| S5 | The fix — same flow, done correctly | /s5-fixed | — |
How the writeup's 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.