URL Origin and CORS Security

May 28, 20266 min read

Cross-Origin Resource Sharing (CORS) is the browser security mechanism that governs whether a web page can make requests to a different origin. Understanding how browsers determine "same-origin" — and how CORS policies allow or block cross-origin requests — is essential for any developer building APIs, SPAs, or microservice architectures.

What Is an Origin

An origin is defined by three URL components: scheme, host, and port.

https://api.example.com:443/data
  ↑        ↑              ↑
scheme    host           port

Two URLs share the same origin if and only if all three components match exactly.

URL AURL BSame Origin?Reason
https://example.com/ahttps://example.com/bYesSame scheme, host, port
https://example.comhttp://example.comNoDifferent scheme
https://example.comhttps://api.example.comNoDifferent host
https://example.comhttps://example.com:8080NoDifferent port
https://example.comhttps://example.com:443Yes443 is default for HTTPS

The path, query string, and fragment do not affect origin. /a and /b on the same origin are same-origin by definition.

The Same-Origin Policy

The same-origin policy (SOP) is the default rule: a script running on one origin cannot read responses from a different origin. This prevents malicious sites from stealing data from authenticated sessions on other sites.

Without SOP, if you visited evil.com, its JavaScript could:

  1. Make a fetch to bank.com/api/balance (your browser includes your bank cookies)
  2. Read the response
  3. Exfiltrate your balance to the attacker

SOP blocks step 2 — the script on evil.com cannot read the response from bank.com because the origins differ.

What SOP Allows

SOP does not block all cross-origin traffic. It specifically blocks reading cross-origin responses. The following are still allowed:

  • Cross-origin navigations: clicking a link or setting window.location
  • Cross-origin form submissions: posting form data to another origin
  • Embedded resources: <img>, <script>, <link>, <video> tags loading cross-origin content
  • Cross-origin writes: POST and PUT requests via fetch or XMLHttpRequest (the request is sent, but the response cannot be read)

This distinction is crucial: SOP prevents data theft, not data sending. CSRF attacks exploit the "writes allowed" gap.

How CORS Works

CORS is a relaxation of SOP — a way for servers to explicitly allow specific origins to read responses. It uses HTTP headers to negotiate access.

Simple Requests

A "simple" request (GET, HEAD, or POST with limited headers) is sent directly. The browser adds an Origin header:

GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com

The server responds with CORS headers:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: true
Content-Type: application/json

If Access-Control-Allow-Origin matches the requesting origin (or is *), the browser allows the script to read the response. If the header is missing or does not match, the browser blocks the response.

Preflight Requests

For non-simple requests (custom headers, PUT/DELETE methods, JSON content type), the browser sends a preflight OPTIONS request first:

OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, X-Auth-Token

The server responds with allowed methods and headers:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, X-Auth-Token
Access-Control-Max-Age: 86400

Only after the preflight succeeds does the browser send the actual request.

Common CORS Mistakes

Wildcard with Credentials

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

This combination is invalid. Browsers reject it because * with credentials would allow any site to make authenticated requests. If you need credentials, you must specify the exact origin.

Reflecting the Origin Header Naively

Some servers echo back whatever Origin header they receive:

# Dangerous!
response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin')

This effectively disables SOP — any origin can read responses. Always validate the origin against an allowlist before reflecting it.

Missing Preflight Handling

If your API does not respond to OPTIONS requests, preflight checks fail, and browsers block the actual request. Ensure your server or API gateway handles OPTIONS for all CORS-enabled routes.

Split Stack Anti-Pattern

When the frontend and API run on different origins (e.g., app.example.com and api.example.com), every API call is cross-origin. This is the most common source of CORS issues. Solutions:

  • Same-origin proxy: serve the API behind the same origin using a reverse proxy (Nginx, Cloudflare Workers)
  • Subdomain CORS: allow https://app.example.com explicitly on the API server
  • Cookie domain: set cookies on .example.com to share auth across subdomains

Key Takeaways

  • Origin is defined by scheme + host + port — path and query do not matter.
  • The same-origin policy blocks reading cross-origin responses, not sending cross-origin requests.
  • CORS headers let servers opt in to cross-origin reads; without them, browsers block responses.
  • Never combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true.
  • Always validate the Origin header against an allowlist — never blindly reflect it.

Try It Yourself

Inspecting URL components to debug a CORS issue? The URL Parser dissects any URL into scheme, host, port, path, and query — so you can quickly identify origin mismatches.