CORS lets a browser on one origin call your API on another. The danger is over-sharing:
Access-Control-Allow-Origin: * exposes your API to every site, and * cannot be combined with
credentials. The safe pattern is to allow one (or a checked list of) origin(s).
Simple: allow ONE known origin
If only https://app.example.com should call your API:
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Nginx:
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Vary "Origin" always;
Apache:
Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Vary "Origin"
Allow a LIST of origins (reflect if matched)
Nginx using a map (only echoes the origin if it’s allowed):
map $http_origin $cors_ok {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
server {
add_header Access-Control-Allow-Origin $cors_ok always;
add_header Vary "Origin" always;
}
With credentials (cookies / auth)
You must name a specific origin (never *) and add:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
Preflight (OPTIONS) for non-simple requests
Browsers send an OPTIONS preflight for custom methods/headers. Answer it:
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_ok always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
Verify
curl -sI -H "Origin: https://app.example.com" https://api.yourdomain.com | grep -i access-control
Never do Access-Control-Allow-Origin: * on an authenticated API, and never blindly reflect
$http_origin without a check — that’s the same as *. Always pair a reflected origin with
Vary: Origin so caches don’t serve one origin’s CORS response to another.