Teams break APIs with CORS either by allowing too much (wildcard + credentials) or blocking valid clients (missing headers on preflight). These examples focus on production-safe defaults, explicit origin allowlists, and predictable credential behavior across four common frameworks.
Also see: How to Fix CORS Misconfiguration, What Is CORS?, and API Security Best Practices.
Universal CORS Security Rules
Before looking at framework-specific examples, internalize these rules. They apply everywhere:
- Never use
*with credentials:Access-Control-Allow-Origin: *combined withAccess-Control-Allow-Credentials: trueis invalid per spec and dangerous. Browsers will block it, but misconfigured servers may still allow exploitation. - Use explicit allowlists: Validate the incoming
Originheader against a hardcoded list. Reflect back only if it matches. - Restrict methods and headers: Only expose the HTTP methods and headers your API actually uses.
PUT,DELETE, and custom headers each expand attack surface. - Separate staging from production origins: Never add production CORS allowlists to staging environments or vice versa.
- Validate at the app layer, not just the gateway: If both Nginx and your app set CORS headers, you get duplicated or conflicting headers. Pick one layer.
Express / Node.js
Use the cors npm package with a dynamic origin function to validate against an allowlist:
// npm install cors
const cors = require('cors');
const ALLOWED_ORIGINS = [
'https://app.example.com',
'https://admin.example.com',
];
const corsOptions = {
origin: function (origin, callback) {
// Allow server-to-server calls (no origin) or allowlisted origins
if (!origin || ALLOWED_ORIGINS.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS policy violation: origin not allowed'));
}
},
credentials: true, // Required for cookies/auth headers
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // Cache preflight 24 hours
};
app.use(cors(corsOptions));
// For specific sensitive routes — stricter policy
const strictCors = cors({
origin: 'https://admin.example.com',
credentials: true,
methods: ['GET', 'POST'],
});
app.use('/api/admin', strictCors);
Common Express CORS mistakes: using origin: true (reflects any origin), forgetting to handle preflight OPTIONS requests, and applying CORS after your auth middleware.
Laravel
Configure CORS in config/cors.php (Laravel 7+). The fruitcake/laravel-cors package is now built in:
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allowed_origins' => [
'https://app.example.com',
'https://admin.example.com',
],
// Use patterns for subdomains (use with caution)
'allowed_origins_patterns' => [
// 'https://*.example.com', // Only if you control all subdomains
],
'allowed_headers' => ['Content-Type', 'X-Requested-With', 'Authorization'],
'exposed_headers' => [],
'max_age' => 86400,
'supports_credentials' => true, // Required for Sanctum SPA auth
];
Ensure the HandleCors middleware is at the top of the middleware stack in app/Http/Kernel.php — before any auth middleware that might reject the preflight.
// app/Http/Kernel.php — in $middleware array
protected $middleware = [
\Illuminate\Http\Middleware\HandleCors::class, // Must be first
\App\Http\Middleware\TrustProxies::class,
// ...
];
Next.js
Configure CORS for API routes in next.config.js headers or per-route in API handlers:
// next.config.js — apply to all API routes
module.exports = {
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Max-Age', value: '86400' },
],
},
];
},
};
For dynamic origin validation (multiple allowed origins) in individual API routes:
// pages/api/data.js — dynamic origin validation
const ALLOWED_ORIGINS = ['https://app.example.com', 'https://admin.example.com'];
export default function handler(req, res) {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin'); // Critical: tell caches origin matters
}
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
return res.status(204).end();
}
// Handle actual request...
res.json({ data: 'ok' });
}
Important: Never use NEXT_PUBLIC_ prefixed environment variables for secrets — they are embedded in client-side bundles. Use server-side env vars for API keys and CORS secrets.
Nginx Reverse Proxy
Centralize CORS at the Nginx layer only if your backend does NOT set its own CORS headers. Mixing both creates duplicate headers that browsers will reject.
# nginx.conf — CORS at proxy level
map $http_origin $cors_origin {
default "";
"https://app.example.com" "https://app.example.com";
"https://admin.example.com" "https://admin.example.com";
}
server {
location /api/ {
# Handle preflight
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' 'Content-Type, Authorization' always;
add_header 'Access-Control-Max-Age' '86400' always;
return 204;
}
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Vary' 'Origin' always;
proxy_pass http://backend;
}
}
The map directive is critical — it returns an empty string for non-allowlisted origins, so no CORS header is added for unknown origins.
Common CORS Mistakes to Avoid
- Reflecting any origin:
Access-Control-Allow-Origin: $http_originwithout validation is equivalent to*and allows any domain to make credentialed requests. - Trusting the Origin header for auth: CORS headers instruct browsers, but server-side code should never use the
Originheader as a security control. Use proper authentication tokens. - Forgetting
Vary: Origin: When you reflect the origin dynamically, you must setVary: Originto prevent CDN/proxy caches from serving one user's CORS response to another. - Duplicate CORS headers: If both Nginx and your app framework set CORS headers, browsers receive two
Access-Control-Allow-Originvalues and reject the response. Choose one layer. - Allowing
nullorigin: Some configurations allowOrigin: null— this can be triggered by sandboxed iframes and file:// requests, creating an unexpected attack vector.
Testing Your CORS Policy
# Test from a non-allowlisted origin — should get no ACAO header
curl -H "Origin: https://evil.com" https://api.example.com/data -v 2>&1 | grep -i "access-control"
# Expected: no output (no ACAO header)
# Test from an allowlisted origin — should reflect back
curl -H "Origin: https://app.example.com" https://api.example.com/data -v 2>&1 | grep -i "access-control"
# Expected: Access-Control-Allow-Origin: https://app.example.com
# Test preflight
curl -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type,Authorization" \
https://api.example.com/data -v 2>&1 | grep -i "access-control"
# Expected: 200/204 with all four CORS response headers
Run the AI QA Monkey API & CORS scanner for automated detection of wildcard origins, credential mismatches, and exposed internal endpoints.
Check your API CORS policy now
Scan for wildcard + credentials risks, exposed endpoints, and high-risk API misconfigurations in under 60 seconds.
Run API & CORS ScanFrequently Asked Questions
Why does Access-Control-Allow-Origin: * fail with credentials?
Per the CORS spec, * cannot be used with Access-Control-Allow-Credentials: true. Browsers enforce this and reject the response. You must explicitly name the origin instead of using the wildcard.
Do I need CORS for same-origin requests?
No. CORS only applies to cross-origin requests — when the requesting page and the API are on different domains, ports, or protocols. Requests from the same origin are not subject to CORS restrictions.
Should I configure CORS in Nginx or in my framework?
Choose one. Framework-level CORS is generally preferred because it has access to your application's origin allowlist and can perform dynamic validation. If you must configure at Nginx, disable CORS in your app framework and use the map pattern shown above.
Can CORS misconfiguration lead to account takeover?
Yes. If your API reflects any origin with credentials enabled, an attacker can host a page that makes credentialed requests to your API, reading sensitive data including authentication tokens, user data, and admin panel contents. See the CORS misconfiguration fix guide for real attack scenarios.