Hardened
S5
Support widget — the same flow, done correctly
Identical traffic to /s1-widget, but every one of the four mistakes is gone. Run the S1 attacker page against this page and the payload is dropped before it reaches the DOM.
// 1. exact allowlist of origins we accept
const ALLOWED_ORIGINS = ["https://widget.pmlab.amir.baby"];
// 2. the one window we are allowed to hear from
const widgetFrame = document.getElementById("widget");
const ALLOWED_SOURCE = widgetFrame.contentWindow;
window.addEventListener("message", function (event) {
// 3. who sent it?
if (!ALLOWED_ORIGINS.includes(event.origin)) {
console.log("Invalid origin!", event.origin);
return;
}
// 4. which window sent it?
if (event.source !== ALLOWED_SOURCE) {
console.log("Invalid source!");
return;
}
// 5. is the payload the shape we expect?
if (typeof event.data !== "string" || event.data.length > 200) {
console.log("Unexpected payload!");
return;
}
// 6. render it as TEXT, never as HTML
document.getElementById("output").textContent = event.data;
});
window.parent.postMessage(message, "https://victim.pmlab.amir.baby"); // never "*"
What changed, and why each one matters
| Fix | Stops |
|---|---|
| Exact origin allowlist | Any other origin, including subdomains of a trusted domain |
event.source check | Another window of the same origin (e.g. an unrelated tab) impersonating the widget |
| Payload shape check | Objects, arrays and oversized strings reaching the sink |
textContent sink | HTML injection, even if every check above were bypassed |
| Explicit target origin on send | The message reaching a window that was never meant to have it |
Defence in depth. Any one of the last three rows would have stopped the
writeup's exploit on its own. The origin allowlist is the primary control; the
safe sink is the one that holds when the primary control is wrong.
Widget output
Raw payload received
—
Embedded widget
Served from https://widget.pmlab.amir.baby — the only origin accepted.