Description
A postMessage receiver that does not strictly validate event.origin, the sending window's scheme, host and port, may trust messages from a malicious site. The attacker must be able to obtain a reference to the target window and deliver a message. If the handler executes message data with eval or performs privileged operations, this may enable feature misuse, data exposure or code execution.
Potential impact
- Untrusted messages may cause sensitive data to be sent elsewhere or exposed in the console or DOM.
- A handler using
eval,Functionor sensitive application functions may execute attacker-selected operations. - Commands may run in an authenticated context and misuse the user's permissions or alter state.
- Excessive messages may overload the event queue and consume browser resources.
Remediation
- Match
event.originexactly against allowed scheme, host and port values. Do not rely on partialstartsWithorincludesmatches. - Where the expected sender is known, also compare
event.sourcewith that window, such as a particulariframe.contentWindow. - Parse and validate
event.dataagainst the expected schema and permitted fields. Do not execute it throughevalornew Function. - Return without performing any action when validation fails. Logging alone does not reject a message.
- When sending sensitive messages, specify the exact
targetOrigininstead of*.
Examples
Before
javascript
// No origin validation before sensitive operations
window.addEventListener("message", (e) => {
// Trust delivered messages without checking the sending window
if (e.data && e.data.type === "setTheme") {
document.documentElement.setAttribute("data-theme", e.data.value);
}
// Dangerous: execute received data
if (e.data && e.data.exec) {
eval(e.data.exec); // An attacker may execute arbitrary code
}
// Set or transmit sensitive data such as tokens
if (e.data && e.data.accessToken) {
localStorage.setItem("access_token", e.data.accessToken);
}
});
After
javascript
// Validate origin, expected source and data schema
(() => {
const ALLOWED_ORIGINS = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
// Retain a reference when the expected sender window is known
const settingsFrame = document.getElementById("settings-frame");
const expectedSource = settingsFrame ? settingsFrame.contentWindow : null;
function isValidPayload(payload) {
// Minimal schema check: require an object and an allowed message type
if (!payload || typeof payload !== "object") return false;
const allowedTypes = new Set(["setTheme", "ping"]);
return typeof payload.type === "string" && allowedTypes.has(payload.type);
}
window.addEventListener("message", (event) => {
// 1) Require an exact origin match
if (!ALLOWED_ORIGINS.has(event.origin)) return;
// 2) Check the sender window when its reference is available
if (expectedSource && event.source !== expectedSource) return;
// 3) Validate and process the data
const data =
typeof event.data === "string"
? (() => {
try {
return JSON.parse(event.data);
} catch {
return null;
}
})()
: event.data;
if (!isValidPayload(data)) return;
switch (data.type) {
case "setTheme":
if (
typeof data.value === "string" &&
/^(light|dark)$/.test(data.value)
) {
document.documentElement.setAttribute("data-theme", data.value);
}
break;
case "ping":
// Reply with the exact validated targetOrigin
event.source &&
event.source.postMessage(
JSON.stringify({ type: "pong" }),
event.origin
);
break;
default:
// no-op
break;
}
});
})();
Explanation:
- Before: The receiver trusts messages without checking their origin, evaluates supplied code and changes settings or tokens. An attacker able to deliver messages may misuse those operations.
- After: The origin must exactly match the allow-list. The sender is also checked when an expected iframe reference exists. If only a particular window is permitted, do not process messages when that reference is missing. Parsed data must satisfy the allowed shape and values; invalid messages return without using dangerous execution APIs.