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:
- On the frontend:
fetch(url, { credentials: 'include' }). - Server response:
Access-Control-Allow-Credentials: true. - 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
OPTIONSor 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,
}
})