CORS and same-origin policy¶
The same-origin policy (SOP) is a browser security model that prevents a script on one origin from reading responses from another. Two URLs share an origin when their scheme, host, and port all match. The policy stops a malicious page from using the victim’s browser as a proxy to read authenticated content from other sites.
Cross-Origin Resource Sharing (CORS) is a controlled relaxation of the SOP. A server adds response headers that tell the browser which other origins may read its responses. Misconfigured CORS undoes the protection SOP provides. The SOP and CORS attack techniques page covers how these misconfigurations are exploited.
The CORS response headers¶
The critical header is Access-Control-Allow-Origin. It specifies which origin the browser will allow to read the
response:
Access-Control-Allow-Origin: https://app.example.com
The wildcard * allows any origin to read the response. It is appropriate for genuinely public resources (a public CDN,
an open API with no authenticated content); it is not appropriate for any endpoint that returns session-specific or
user-specific data.
Access-Control-Allow-Credentials: true allows the browser to include cookies and other credentials in a cross-origin
request. The combination of Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true is rejected by
browsers: a server that wants credentialed cross-origin requests only accepts an explicit origin, not a wildcard.
Allowlisting origins¶
The correct pattern for multi-origin CORS is an explicit server-side allowlist, not a reflection of whatever Origin
header the request sends:
ALLOWED_ORIGINS = {
"https://app.example.com",
"https://admin.example.com",
}
def set_cors_headers(response, request_origin: str):
if request_origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = request_origin
response.headers["Vary"] = "Origin"
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
The Vary: Origin header is important. Without it, a caching layer may serve a response with one origin’s CORS headers
to a request from a different origin.
The unsafe pattern for comparison:
# unsafe: reflects any origin the client sends
origin = request.headers.get("Origin", "")
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
An attacker controlled page can send any Origin header. Reflecting it back grants that page access to authenticated
content.
The null origin¶
Access-Control-Allow-Origin: null is not safe for authenticated endpoints. The null origin is sent by sandboxed
iframes, data: URLs, and file:// pages. An attacker can trigger a null origin from a sandboxed iframe:
<iframe sandbox="allow-scripts allow-forms" src="data:text/html,..."></iframe>
A server that trusts null can be targeted from any page that can embed an iframe.
postMessage¶
Applications that use postMessage for cross-frame or cross-window communication validate the event.origin before
processing the message:
window.addEventListener("message", (event) => {
if (event.origin !== "https://trusted.example.com") {
return; // ignore messages from untrusted origins
}
// process event.data
});
An event listener that does not check the origin accepts messages from any window, including attacker-controlled pages that have opened the application as a popup or embedded it in an iframe.
JSONP¶
JSONP (JSON with Padding) is a legacy pattern for cross-origin data loading that predates CORS. It works by returning a
JavaScript function call wrapping JSON data. The caller’s domain can read the response because it arrives via a
<script> tag, which is not subject to SOP.
JSONP is not appropriate for authenticated or sensitive data. Modern applications on supported browsers use CORS instead.