CORS Cross-Origin Security Configuration: From Mechanism to Nginx Hardening
Misconfigured CORS is a common, stealthy Web flaw: an overly wide cross-origin policy lets attackers steal authenticated user data. This guide covers CORS mechanics, Nginx and server-side secure config templates, and how to verify them.
CORS is a high-frequency, stealthy piece of the Web security baseline. Mis-setting Access-Control-Allow-Origin: * or reflecting arbitrary Origin lets an attacker read the victim's authenticated data from any site via JavaScript, leading to account takeover and data leaks. This article gives a secure config from mechanism through Nginx practice.
Scope
- Split frontend/backend (
app.example.comfrontend,helixcyber.aiAPI) cross-origin APIs - Nginx reverse-proxy API gateway where CORS headers must be managed at the gateway
- Pentest findings of overly broad
Access-Control-Allow-Originthat need tightening
Prerequisites
- Nginx 1.16+ (syntax here is compatible with nginx 1.18 1.26)
- Permission to edit Nginx config and run
nginx -s reload - An agreed list of legitimate cross-origin sources (frontend domain list)
Mechanism
Browsers' same-origin policy blocks cross-origin response reads by default. CORS is the server explicitly allowing those reads. Three response headers matter:
Access-Control-Allow-Origin(ACAO): allowed origin; must be a single concrete Origin or *, never an "arbitrary reflection" built from a variable.Access-Control-Allow-Credentials: when true, cookies may be sent; ACAO must never be * and must match the origin exactly.Access-Control-Allow-Methods / Headers: allowed methods and custom headers, used withOPTIONSpreflight.
Common vuln patterns: ① ACAO always * with Allow-Credentials=true (browsers actually reject this, but many servers reflect Origin + always true, which opens every origin); ② reflect request Origin with no whitelist — attacker on evil.com sends a credentialed request, server echoes that Origin and allows the response to be read, so user data is stolen cross-origin.
Procedure
1. Whitelist check + exact echo (Nginx gateway)
# /etc/nginx/conf.d/cors.conf —— include in the server block or use inside location
map $http_origin $cors_origin {
default "";
"~^https?://(www\\.)?example\\.com$" $http_origin;
"~^https?://admin\\.example\\.com$" $http_origin;
}
server {
listen 443 ssl;
server_name helixcyber.ai;
location / {
# emit CORS headers only when origin hits the whitelist
if ($cors_origin) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;
}
# preflight: return 204 immediately
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
add_header Access-Control-Max-Age 86400 always;
add_header Vary Origin always;
return 204;
}
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
}
Key points: map whitelist uses regex for exact domain match; on miss $cors_origin is empty, no CORS headers are emitted, browser blocks; Vary: Origin makes CDN cache vary by origin.
2. Server-side second check (Java / Python example)
# Python Flask example: echo exact Origin after whitelist check
from flask import request, make_response
ALLOWED_ORIGINS = {"https://app.example.com", "https://admin.example.com"}
@app.after_request
def set_cors(resp):
origin = request.headers.get("Origin")
if origin in ALLOWED_ORIGINS:
resp.headers["Access-Control-Allow-Origin"] = origin
resp.headers["Access-Control-Allow-Credentials"] = "true"
resp.headers["Vary"] = "Origin"
return resp
3. Fallback for generic requests
- Cookie-free pure APIs: can simplify to a fixed whitelist Origin without Allow-Credentials.
- Same-site reads only: prefer
SameSitecookies + same-origin reverse proxy to eliminate the cross-origin surface. - Don't let a compromised subdomain expand risk: don't write the whitelist as
~^https?://.*example\\.com$(wildcard subdomains). Ifevil.example.comis taken over (e.g. dangling DNS), it can also read data cross-origin.
Verification
# 1. Whitelisted origin: should return exact Origin
curl -sI -H "Origin: https://app.example.com" https://helixcyber.ai/api/user | grep -i "access-control\\|vary"
# 2. Non-whitelisted origin: Access-Control-Allow-Origin must be absent
curl -sI -H "Origin: https://evil.com" https://helixcyber.ai/api/user | grep -i "access-control" || echo "PASS: no CORS headers emitted"
# 3. Preflight verification
curl -sI -X OPTIONS -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: GET" https://helixcyber.ai/api/user | grep -i "allow-methods\\|allow-headers\\|max-age"
# 4. Credentialed request: confirm ACAO is not *
curl -sI -H "Origin: https://app.example.com" -H "Cookie: session=test" https://helixcyber.ai/api/user | grep -i "allow-credentials"
Also verify in the browser console with fetch:
fetch("https://helixcyber.ai/api/user", {credentials: "include"})
.then(r => r.json()).then(console.log)
# If the network panel reports a CORS error → config is taking effect; if data is readable → confirm the origin is already on the whitelist
FAQ
Q1: Why still getting an "Access-Control-Allow-Origin" error after configuring CORS?
Common causes: ① OPTIONS preflight was intercepted by Nginx if before headers were set — confirm the OPTIONS branch is before proxy_pass and nothing besides return interferes; ② multiple add_header sources — Nginx only applies all of them with always or within the same inheritance block; without always, other add_header in the same location override; ③ CDN/WAF stripped custom headers — check response headers at every hop.
Q2: Can ACAO be set to multiple domains?
No. ACAO accepts a single value or *. Multiple origins must use "server checks Origin against whitelist → echo the concrete Origin" (the map + exact echo in this article). That is the standard, safe approach.
Q3: How to find existing CORS misconfigurations?
Use Burp Suite's CORS Scanner extension or test manually: set Origin to evil.com, null, and https://example.com.evil.com and watch the response headers. If any value is echoed and Allow-Credentials=true, it is a reflected CORS vuln and belongs on the fix list.
Summary
CORS security in one sentence: echo Origin exactly for whitelist sources only; never reflect arbitrarily. Nginx map whitelist + preflight handling + Vary header converges all cross-origin policy at the gateway; add a server-side second check as belt-and-suspenders. After the change, regression-test with whitelist / non-whitelist / credentialed requests so production policy matches intent.