ASVS 5.0 requirement V3.5.5 says messages received through the postMessage interface must be discarded if the origin of the message is not trusted, or if the message syntax is invalid. It is a Level 2 requirement.
That covers two checks in every message listener: compare event.origin to an exact trusted value, then validate the data before using it.
Why it matters
window.postMessage lets pages in different origins talk, for example a parent page and an embedded widget or a sign in popup. A listener without checks accepts messages from any window that has a reference to yours, and any site can open or embed your page and send it data. See CWE-346 for origin validation errors.
What happens next depends on your handler. If it writes event.data.html into innerHTML, you have DOM XSS. If it reads event.data.redirect and sets location, you have an open redirect. If it treats a message as "user is authenticated" or "payment complete", an attacker can fake the state.
How to implement it
A defensive listener checks origin first, then shape, then acts:
const TRUSTED = "https://widget.example.com";
window.addEventListener("message", (event) => {
if (event.origin !== TRUSTED) return; // exact match, never indexOf or endsWith
const msg = event.data;
if (typeof msg !== "object" || msg === null) return;
if (msg.type !== "resize" || !Number.isInteger(msg.height)) return;
if (msg.height < 0 || msg.height > 5000) return;
document.getElementById("frame").style.height = msg.height + "px";
});
Notes on that code:
- Compare with
!==against a full origin string. Substring checks accepthttps://widget.example.com.evil.net. - Validate type, fields and ranges, and ignore anything unexpected. Do not pass data to
innerHTML,evalorlocation. - Use
textContentif you must display a string.
When sending, always name the target origin and never use the wildcard:
iframe.contentWindow.postMessage({ type: "init", userId: 42 }, "https://widget.example.com");
A "*" target means any origin that now occupies that window receives the data.
If a message carries something sensitive, also check event.source === iframe.contentWindow so you only accept messages from the frame you created.
Click through it at your own pace, or open it full screen.
How to test it
Static review first: search the codebase for addEventListener("message" and onmessage, and confirm each handler begins with an origin check. A code scan finds listeners lacking one; the demo above shows how Safeguard's static analysis surfaces them.
Then test dynamically. Host a page on a different origin that opens or frames yours and sends messages from the console:
const w = window.open("https://app.example.com/");
setTimeout(() => {
w.postMessage({ type: "resize", height: 99999 }, "*");
w.postMessage({ type: "resize", height: "<img src=x onerror=alert(1)>" }, "*");
w.postMessage("not an object", "*");
}, 3000);
None of these may change the page, throw an uncaught error, or run script. Repeat with malformed JSON strings if your handler parses text.
Common mistakes
No origin check at all. The most common issue.
Loose matching. indexOf, endsWith, regexes without anchors.
Trusting data after the origin check. A trusted origin can still be compromised or buggy, so validate shape anyway.
Wildcard target in postMessage. Always specify the exact origin.
Checking event.origin after using the data. Order matters.
Quick check
Is checking event.source enough?
No. It tells you which window sent the message, not which origin that window currently has. Check origin too.
What if the sender is another page of mine?
Still check the origin. Your own pages can contain XSS, and the check keeps one compromised page from commanding another.
Sources
- OWASP ASVS 5.0, requirement V3.5.5
- MDN: Window.postMessage
- CWE-346 on Gold