Skip to main content

What is CORS?

CORS is the browser's answer to the question "Is this third-party site allowed to talk to my API?" — and one of the most common stumbling blocks in modern web apps.

Same-Origin Policy

By default, browsers may only make requests to the same origin (scheme, host, port) the page came from. A page on https://app.example.com may therefore not simply call https://api.example.com via fetch() — that would be cross-origin. The Same-Origin Policy is the most important protection against unauthorized API calls from foreign sites (for example phishing pages calling your banking API in the background).

What is CORS for?

CORS is the controlled relaxation mechanism: as the API operator, you tell the browser "Yes, this third-party site may call me — but only this one." The browser asks the server upfront whether the request is allowed and only carries it out if the server agrees. CORS therefore does not protect your API — anyone can hit it with curl — it protects your users from third-party sites making API calls on their behalf.

Simple requests

For simple GET or POST requests (with standard content types), the browser just sends the request and inspects the response header Access-Control-Allow-Origin:

Access-Control-Allow-Origin: https://app.example.com

If the calling page's origin matches, JavaScript may read the response. Otherwise the browser blocks access (the request itself was already executed though — important: GET requests must never have side effects!).

Preflight requests

For "non-simple" requests (PUT, DELETE, custom headers, JSON POSTs etc.) the browser first sends an OPTIONS request to the server (preflight) asking: "May I make the following request?" Only if the server agrees does the actual request go through.

Server response to the preflight:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Max-Age tells the browser how long it may cache the preflight response — set this to 24h so not every request costs an extra OPTIONS roundtrip.

Cookies and auth

By default, the browser sends no cookies on cross-origin requests. If you want it to (for example because your API uses session cookies), three things must line up:

  1. On the frontend: fetch(url, { credentials: 'include' }).
  2. Server response: Access-Control-Allow-Credentials: true.
  3. The server must not use Access-Control-Allow-Origin: * — the origin must be specified explicitly.

Common mistakes

  • Allow-Origin: * on an API with auth: dangerous, because every site can access it. Allow-Origin should come from a whitelist.
  • Mirroring the origin: Access-Control-Allow-Origin: ${request.origin} without validation — grants access to any caller. Validate against a real whitelist.
  • OPTIONS not answered: if your server ignores OPTIONS or replies with 405, the preflight fails and the actual request never runs.
  • Treating CORS as CSRF protection: CORS does not protect against CSRF — an attacker can still send POST requests with cookies via a form. You need CSRF tokens.

Configuration example (Flask)

from flask_cors import CORS

CORS(app, resources={
    r"/api/*": {
        "origins": ["https://app.example.com"],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "allow_headers": ["Content-Type", "Authorization"],
        "supports_credentials": True,
        "max_age": 86400,
    }
})

Check it yourself: HTTP-Header

HSTS, CSP, X-Frame-Options & Co. Enter a domain and see in seconds how your CORS is doing.

Also in the glossary