Missing origin validation when receiving postMessage events

Missing origin validation when receiving postMessage events

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, Function or 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.origin exactly against allowed scheme, host and port values. Do not rely on partial startsWith or includes matches.
  • Where the expected sender is known, also compare event.source with that window, such as a particular iframe.contentWindow.
  • Parse and validate event.data against the expected schema and permitted fields. Do not execute it through eval or new Function.
  • Return without performing any action when validation fails. Logging alone does not reject a message.
  • When sending sensitive messages, specify the exact targetOrigin instead 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.

References