Sitemap

Same Origin Policy and PostMessage

9 min readMar 9, 2026

--

Hello Learners, You are authorized to know me as Obfuscator!

Today we are going to dive into the concept about Same Origin Policy (SOP) and PostMessage.

Same-Origin Policy (SOP)

The same-origin policy is a foundational browser security mechanism that isolates web content by restricting how documents or scripts loaded from one origin can interact with resources from another origin. An origin is defined as the combination of three components: the URI scheme (protocol, e.g., http or https), the host (domain name), and the port number (default 80 for http, 443 for https if not specified). Two resources share the same origin only if all three match exactly. This policy prevents malicious scripts on one site from reading or manipulating sensitive data on another site, such as accessing your logged-in Gmail inbox or Facebook messages from a rogue website.
Without the same-origin policy, visiting a malicious page could allow its JavaScript to silently send requests to other sites where you’re authenticated (e.g., your bank or email provider). Browsers automatically include relevant cookies (including session cookies) with those requests, so the server would respond with user-specific data within your active session. The SOP blocks unauthorized cross-origin reads via APIs like XMLHttpRequest or Fetch, protecting against data exfiltration while still permitting certain safe interactions (like loading images or scripts for display).
Here are common origin comparison examples (assuming base URL: http://normal-website.com/example/page.html):

Yet some measures are there to loosen the SOP rule to allow certain things, some general restrictions that SOP implements are:

  • Spying on private data: Can’t read your emails, messages, bank balances, photos, or any personal info from another site.
  • Peeking or editing content: Can’t view, copy, or change what’s displayed on a different page (e.g., no altering forms on your online shopping cart).
  • Stealing login info: Can’t grab or use your cookies, sessions, or “stay logged in” status from sites like Facebook, YouTube, or Twitter using another site’s JavaScript.
  • Reading cross-site responses: JavaScript can sometimes send a request to another site, but you usually can’t see or use the reply data. There needs to be proper CORS implementation allowing for that to read.
  • Controlling embeds: Can’t take over or inspect iframes like embedded maps, videos, or payment windows from other domains.

The same-origin policy primarily restricts read access via JavaScript APIs (Fetch, XMLHttpRequest, DOM manipulation across frames). However, it is intentionally more permissive for embedding and loading resources so the web can function as it always has (e.g., CDNs, third-party widgets). Cross-origin embedding is generally allowed, but the content cannot be read by JavaScript unless the server explicitly permits it via CORS headers.

Here are the main resources that browsers allow to be embedded or loaded cross-origin:

  • <script src=”…”> → Loads and executes external JavaScript (syntax error details are hidden for cross-origin scripts)
  • <link rel=”stylesheet” href=”…”> → Applies external CSS (requires correct Content-Type: text/css and valid CSS syntax)
  • <img src=”…”> or CSS background-image → Displays images (pixel data blocked unless CORS-enabled)
  • <video src=”…”> and <audio src=”…”> → Plays media (limited metadata access)
  • <object> and <embed> → Embeds plugins or external content (e.g., PDFs)
  • @font-face { src: url(https://medium.com/%E2%80%A6); } → Loads web fonts (modern browsers usually require CORS headers)
  • <iframe src=”…”> → Embeds entire pages (parent cannot read iframe DOM; can be blocked with X-Frame-Options or CSP frame-ancestors)

Cross Origin Resource Sharing (CORS)

We have already read the term CORS in above section which may have made some confusion. Lets dive into it here.

CORS (Cross-Origin Resource Sharing) is a browser security feature that controls how web pages from one origin (domain + protocol + port) can request resources from a different origin. It builds on the Same-Origin Policy (SOP), which blocks most cross-origin interactions by default to prevent malicious sites from stealing data (like reading your emails while you’re logged into another tab).
Without CORS, a frontend app at https://myapp.com couldn’t fetch data from your API at https://api.myapp.com — even though they’re on the same domain — because the origins differ slightly. CORS lets servers explicitly allow these cross-origin requests by sending special response headers, making modern web apps (SPAs, mobile backends, third-party APIs) possible.

The following image describe simple CORS implementation, but not limited to this concept:

Press enter or click to view image in full size
CORS process

When a browser-based JavaScript application sends a cross-origin GET request to an API, the browser automatically adds an Origin header indicating the requesting site. The API server processes the request and returns a 200 OK response along with the Access-Control-Allow-Origin (ACAO) header. The browser then performs a CORS check: if the ACAO value matches the requesting origin (or is explicitly allowed), the response is considered safe. As a result, the browser exposes the response data to JavaScript, allowing the application to read and process the API data; otherwise, the response is blocked from JS access despite being received by the browser.

Just like Access-Control-Allow-Origin (often abbreviated as ACAO) is the key header that tells the browser which origins are allowed to read the response, CORS involves several other important response headers that the server sends to control cross-origin access.

Press enter or click to view image in full size
CORS response headers

Using wildcard (*) with credentials is not allowed
When Access-Control-Allow-Credentials: true is present (meaning cookies, HTTP authentication, or client certificates can be included), the browser rejectsAccess-Control-Allow-Origin: *.
This combination is forbidden — the request will fail with a CORS error even if everything else is correct.

Pre-flight request

A preflight request is an automatic OPTIONS request the browser sends before certain cross-origin requests to check if the server allows them. It only happens for non-simple (complex) requests and acts as a permission check.

When a Preflight Is Sent The browser sends a preflight if the request is cross-origin AND meets any of these:

  • HTTP method is not GET, HEAD, or POST (e.g., PUT, DELETE, PATCH)
  • Content-Type is not one of: application/x-www-form-urlencoded, multipart/form-data, text/plain (e.g., application/json)
  • Any custom header is used (not in the safelist: Accept, Content-Language, etc.)

If none apply → simple request → no preflight.

What Happens in a Preflight

  1. Browser sends OPTIONS with:
  • Origin
  • Access-Control-Request-Method (intended method)
  • Access-Control-Request-Headers (custom headers, if any)

2. Server must reply (200/204) with:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods (includes requested method)
  • Access-Control-Allow-Headers (includes requested headers)
  • Optional: Access-Control-Max-Age, Access-Control-Allow-Credentials

3. If approved → actual request is sent. If not → preflight fails → actual request is blocked (never sent).

Press enter or click to view image in full size
Pre flight request example (Src: Rapid)

Concept of PostMessage

Despite the strict restrictions of the Same-Origin Policy (SOP), which prevents JavaScript from one origin reading or manipulating content from another origin, there is one important built-in mechanism that allows controlled cross-origin communication: the window.postMessage() API.

Get Saksham Baral’s stories in your inbox

Join Medium for free to get updates from this writer.

postMessage lets a page (or iframe/popup) send messages to a window from a different origin in a secure, explicit way. It bypasses the SOP’s read restrictions for this specific purpose, but only when both sides agree on the rules, making it the standard way modern web apps talk between domains (e.g., parent page ↔ embedded iframe, or main app ↔ authentication popup).

How postMessage Actually Works

window.postMessage() is a Web API method that enables secure cross-origin communication between Window objects, such as between a parent page and an iframe, a page and a popup, or across tabs/windows. It bypasses the same-origin policy (SOP) in a controlled manner, allowing data exchange even between different domains, while relying on developers to enforce security checks. Introduced in HTML5, it’s defined in the HTML Living Standard and widely supported across modern browsers since around 2015.

Sending a Message

One window sends data to a target window using this syntax:

targetWindow.postMessage(message, targetOrigin, [transfer]);
// or with options object (alternative syntax)
targetWindow.postMessage(message, { targetOrigin: 'https://example.com', transfer: [transferable] });
  • targetWindow: A reference to the receiving Window object (e.g., iframe.contentWindow, window.opener, or from window.open()).
  • message: The data to send. This can be any serializable value (string, object, array, etc.). The browser serializes it using the structured clone algorithm, which supports complex data like Date, Blob, File, ArrayBuffer, but not functions or DOM nodes. This creates a safe, deep copy — cyclic structures are handled, but some objects may throw errors if not cloneable.
  • targetOrigin (optional): The exact origin (protocol + hostname + port, e.g., “https://trusted.com:443") the receiver must match for delivery. Defaults to “/” (sender’s origin). Use “*” to allow any origin, but this is strongly discouraged for sensitive data as it risks interception. For data: or file: URLs, “*” is required due to opaque origins.
  • transfer (optional): An array of transferable objects (e.g., ArrayBuffer, MessagePort, SharedArrayBuffer). Ownership transfers to the receiver- these become unusable on the sender side, optimizing for large data without copying. For SharedArrayBuffer, cross-origin isolation (via COOP/COEP headers) is mandatory.

The method queues a task to dispatch a MessageEvent to the target. If origins don’t match (unless “*”), the message is discarded silently.

Receiving a Message

The receiving window listens for the ‘message’ event:

window.addEventListener('message', (event) => {
// Security checks first!
if (event.origin !== 'https://expected-sender.com') {
return; // Discard untrusted messages
}
if (event.source !== expectedSourceWindow) {
return; // Optional: Verify sender reference
}

// Validate data structure
if (typeof event.data !== 'object' || !event.data.action) {
return; // Discard malformed data
}

// Now safe to use
console.log('Received:', event.data);
});

The browser provides these MessageEvent properties (enforced and unspoofable by JavaScript):

  • event.origin: The sender’s origin at dispatch time (e.g., “https://sender.com"). This is a snapshot — subsequent navigations don’t affect it. Cannot be faked; the browser sets it based on the sending window’s state. For IDN domains, it may be Unicode or punycode — check both.
  • event.data: The deserialized message (via structured clone).
  • event.source: Reference to the sending WindowProxy (for replies). Null in browser extensions for security.
  • event.ports: Array of transferred MessagePort objects (for channel messaging).

Events fire asynchronously after synchronous code completes.

Security Rules : Follow These Strictly

postMessage is secure only with proper implementation. Browsers enforce origin invariants, but developers must handle validation. Failures lead to XSS, token theft, or data leaks. User agents may rate-limit cross-origin messages to prevent DoS.

Always Validate Sender on Receive

  • Use exact string comparison for event.origin against a whitelist.
  • Optionally verify event.source for bidirectional trust.
  • Bad (exploitable):
    - No check.
    - Loose: event.origin.includes(‘example.com’) — bypassed by evil.com/example.com.
    - Regex pitfalls: Weak patterns allow similar domains.
  • Good:
const allowedOrigins = ['https://app.example.com', 'https://auth.example.com'];
window.addEventListener('message', (event) => {
if (!allowedOrigins.includes(event.origin)) return;
// Proceed safely
if (event.data.action === 'login') {
handleLogin(event.data.token);
}
});

Always Specify Exact targetOrigin on Send (Avoid “*”)

  • Bad: iframe.contentWindow.postMessage(token, ‘*’); — Leaks to any listener (e.g., malicious iframes/popups).
  • Good: iframe.contentWindow.postMessage(token, ‘https://child.example.com');
  • Use “*” only for non-sensitive data or opaque origins (data:, file:).

Treat Received Data as Untrusted Input

  • Even after origin check: Validate type/structure (e.g., typeof event.data === ‘object’ && event.data.hasOwnProperty(‘key’)).
  • Sanitize before use: Never inject into innerHTML, eval(), document.write(), or URLs without escaping. Use textContent over innerHTML for DOM updates.
  • If sender is compromised (e.g., via XSS), data could be malicious.

Additional Best Practices

  • Add listeners only if expecting cross-origin messages.
  • Implement rate-limiting in code to avoid DoS.
  • For extensions: Use runtime.sendMessage() instead; event.source is null.
  • Sandbox iframes for untrusted content.

Common Real-World Attacks

Where postMessage Is Used Every Day

  • OAuth/SSO logins: Send auth codes/tokens from popups back to parent.
  • Embedded widgets: Payments (Stripe), maps (Google), chats (Intercom).
  • Subdomain apps: app.company.com communicates with dashboard.company.com.
  • Iframe resizing: Dynamic height adjustment.
  • Tab/window sync: Same-app coordination (e.g., session sharing).
  • Extensions: Content script to background (though alternatives preferred).

Quick Summary Table

Press enter or click to view image in full size
Figure: Quick Summary Table

This much for today.

Dropping comment does not require authentication, feel free . . .

Meet you soon again.

--

--