Skip to main content
interlace
Plugin: browser-securityRules

require-postmessage-origin-check

Detects postMessage event handlers without origin validation

CWE: CWE-346
OWASP Mobile: OWASP Mobile Top 10

Detects postMessage event handlers without origin validation. This rule is part of eslint-plugin-browser-security.

⚠️ This rule errors by default in the recommended config.

Quick Summary

AspectDetails
CWE ReferenceCWE-346 (Origin Validation Error)
Severity🔴 High
Auto-Fix✅ Yes (suggests origin check)
CategorySecurity
Best ForApps using iframes, cross-window communication

Vulnerability and Risk

Vulnerability: Accepting postMessage events without validating the origin allows any website to send messages to your application.

Risk: Attackers can:

  • Inject malicious data/commands
  • Trigger unauthorized actions
  • Bypass authentication

PostMessage Attack Flow

Examples

❌ Incorrect

// No origin check - VULNERABLE
window.addEventListener('message', (event) => {
  const { action, data } = event.data;
  processAction(action, data); // Anyone can trigger this!
});

// Empty origin check - VULNERABLE
window.addEventListener('message', (event) => {
  if (event.origin) {
    // This is always truthy!
    processAction(event.data);
  }
});

// The onmessage property registers the same cross-window listener
window.onmessage = (event) => {
  processAction(event.data);
};

// includes() on a STRING is a substring test: 'https://trusted.co' passes
window.addEventListener('message', (event) => {
  if (!'https://trusted.com'.includes(event.origin)) return;
  processAction(event.data);
});

✅ Correct

// Exact comparison
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted.com') return;
  processAction(event.data);
});

// Exact-match allowlist: Array includes() / Set has() compare whole strings
const ALLOWED = ['https://trusted.com', 'https://app.trusted.com'];
window.onmessage = (event) => {
  if (!ALLOWED.includes(event.origin)) return;
  processAction(event.data);
};

// A fully anchored, dot-escaped origin pattern
window.addEventListener('message', (event) => {
  if (!/^https:\/\/trusted\.com$/.test(event.origin)) return;
  processAction(event.data);
});

// A WebSocket / Worker / BroadcastChannel message has no origin to check
const ws = new WebSocket('wss://api.example.com');
ws.onmessage = (event) => render(event.data);

Message listeners are recognised as addEventListener('message', fn) on any receiver not proven to be a WebSocket, EventSource, Worker, SharedWorker, BroadcastChannel or MessagePort, and as window.onmessage / self.onmessage / globalThis.onmessage / a bare global onmessage = fn. A handler passed by reference is not analysed.

Options

OptionTypeDefaultDescription
allowInTestsbooleanfalseSkip this rule in *.test.* / *.spec.* files
{
  "rules": {
    "browser-security/require-postmessage-origin-check": "error"
  }
}

Best Practices

1. Use Allowlist Pattern

const ALLOWED_ORIGINS = new Set([
  'https://parent.example.com',
  'https://embed.example.com',
]);

function handleMessage(event) {
  if (!ALLOWED_ORIGINS.has(event.origin)) {
    return;
  }
  // Process message
}

2. Validate Message Structure

window.addEventListener('message', (event) => {
  // 1. Check origin
  if (event.origin !== 'https://trusted.com') return;

  // 2. Validate message structure
  if (!event.data || typeof event.data.action !== 'string') {
    console.error('Invalid message structure');
    return;
  }

  // 3. Whitelist allowed actions
  const ALLOWED_ACTIONS = ['resize', 'navigate', 'close'];
  if (!ALLOWED_ACTIONS.includes(event.data.action)) {
    console.error('Unknown action:', event.data.action);
    return;
  }

  processAction(event.data);
});

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Handler in Separate Function

Why: Origin check in called function not visible.

// ❌ NOT DETECTED - Check in handler
window.addEventListener('message', handleMessage); // validates internally

Mitigation: Apply rule to handler implementations.

Dynamic Event Registration

Why: Events registered dynamically may not be detected.

// ❌ NOT DETECTED - Dynamic registration
const eventType = 'message';
window.addEventListener(eventType, handler);

Mitigation: Use static event registration.

Weak Origin Checks

Why: Check quality is assessed only for includes() on a string literal (a substring test) and for regexp anchoring. A substring test ON the origin string is still read as a check.

// ❌ NOT DETECTED - Weak check
if (event.origin.includes('trusted')) {
  // Insecure!
  processAction(event.data);
}

Mitigation: Use exact origin comparison.

Library Abstractions

Why: Third-party message handlers not analyzed.

// ❌ NOT DETECTED - Library abstraction
messageLib.onMessage((data) => process(data)); // No origin check visible

Mitigation: Review library security. Configure message handlers.

Resources

Use this rule

npm install -D oxlint-plugin-browser-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-browser-security"],  "rules": {    "browser-security/require-postmessage-origin-check": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-browser-security'],    rules: {      'browser-security/require-postmessage-origin-check': 'error',    },  },});

oxlint-plugin-browser-security re-exports eslint-plugin-browser-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.