CORS Advanced
Advanced Cross-Origin Resource Sharing patterns and security
CORS Advanced
Cross-Origin Resource Sharing (CORS) controls which origins can access your API. Misconfigured CORS is a common security vulnerability.
The Same-Origin Policy
Without CORS, browsers block cross-origin requests:
// On https://app.example.com
fetch('https://api.different.com/data')
.then(res => res.json())
.catch(err => {
// ❌ CORS error: Blocked by Same-Origin Policy
});CORS Basics
Simple Request
GET /data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Response:
Access-Control-Allow-Origin: https://app.example.comPreflight Request
For non-simple requests (PUT, DELETE, custom headers):
OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-Custom-Header
Response:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 86400CORS Headers
Access-Control-Allow-Origin
// ❌ DANGEROUS: Allows all origins
res.setHeader('Access-Control-Allow-Origin', '*');
// ✅ GOOD: Specific origin
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
// ✅ GOOD: Multiple origins (dynamic)
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
const origin = req.headers.origin;
if (origin && allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}Access-Control-Allow-Credentials
// Allow cookies/auth headers
res.setHeader('Access-Control-Allow-Credentials', 'true');
// ❌ DANGEROUS: Cannot use '*' with credentials
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true'); // Won't work!
// ✅ GOOD: Specific origin with credentials
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');Access-Control-Allow-Methods
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');Access-Control-Allow-Headers
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');Access-Control-Expose-Headers
// Expose custom headers to client
res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count, X-Page-Number');Access-Control-Max-Age
// Cache preflight for 24 hours
res.setHeader('Access-Control-Max-Age', '86400');Next.js Implementation
API Route
// app/api/data/route.ts
import { NextRequest, NextResponse } from 'next/server';
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,
].filter(Boolean) as string[];
export async function GET(request: NextRequest) {
const origin = request.headers.get('origin');
const response = NextResponse.json({ data: 'Hello' });
if (origin && allowedOrigins.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Credentials', 'true');
}
return response;
}
export async function OPTIONS(request: NextRequest) {
const origin = request.headers.get('origin');
const response = new NextResponse(null, { status: 204 });
if (origin && allowedOrigins.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
response.headers.set('Access-Control-Max-Age', '86400');
response.headers.set('Access-Control-Allow-Credentials', 'true');
}
return response;
}Middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin');
const isAllowedOrigin = origin && allowedOrigins.includes(origin);
// Handle preflight
if (request.method === 'OPTIONS') {
const response = new NextResponse(null, { status: 204 });
if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
response.headers.set('Access-Control-Max-Age', '86400');
response.headers.set('Access-Control-Allow-Credentials', 'true');
}
return response;
}
const response = NextResponse.next();
if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Credentials', 'true');
}
return response;
}
export const config = {
matcher: '/api/:path*',
};Express.js
import express from 'express';
import cors from 'cors';
const app = express();
// Option 1: Simple (allows all origins)
app.use(cors());
// Option 2: Specific origins
const corsOptions = {
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
optionsSuccessStatus: 200,
};
app.use(cors(corsOptions));
// Option 3: Dynamic origin validation
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count'],
maxAge: 86400,
};
app.use(cors(corsOptions));Advanced Patterns
Origin Validation with Regex
function isAllowedOrigin(origin: string): boolean {
const allowedPatterns = [
/^https:\/\/.*\.example\.com$/, // Any subdomain
/^https:\/\/app-\d+\.example\.com$/, // Versioned subdomains
];
return allowedPatterns.some(pattern => pattern.test(origin));
}
// Use in middleware
const origin = request.headers.get('origin');
if (origin && isAllowedOrigin(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
}Conditional CORS
// Different CORS rules for different routes
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const origin = request.headers.get('origin');
let allowedOrigins: string[] = [];
if (pathname.startsWith('/api/public')) {
// Public API: Allow more origins
allowedOrigins = ['https://app.example.com', 'https://partner.com'];
} else if (pathname.startsWith('/api/admin')) {
// Admin API: Strict
allowedOrigins = ['https://admin.example.com'];
}
const response = NextResponse.next();
if (origin && allowedOrigins.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
}
return response;
}CORS with Authentication
export async function POST(request: NextRequest) {
const origin = request.headers.get('origin');
const authToken = request.headers.get('authorization');
// Validate origin
if (!origin || !allowedOrigins.includes(origin)) {
return new NextResponse('Forbidden', { status: 403 });
}
// Validate auth
if (!authToken || !await validateToken(authToken)) {
return new NextResponse('Unauthorized', { status: 401 });
}
const response = NextResponse.json({ success: true });
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Credentials', 'true');
return response;
}Rate Limiting per Origin
const rateLimits = new Map<string, { count: number; resetAt: number }>();
function checkRateLimit(origin: string): boolean {
const now = Date.now();
const limit = rateLimits.get(origin);
if (!limit || now > limit.resetAt) {
rateLimits.set(origin, { count: 1, resetAt: now + 60000 });
return true;
}
if (limit.count >= 100) { // 100 requests per minute
return false;
}
limit.count++;
return true;
}
export async function GET(request: NextRequest) {
const origin = request.headers.get('origin');
if (!origin || !checkRateLimit(origin)) {
return new NextResponse('Too Many Requests', { status: 429 });
}
// Process request...
}Security Best Practices
1. Never Use Wildcard with Credentials
// ❌ DANGEROUS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// ✅ SECURE
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');2. Validate Origin Strictly
// ❌ DANGEROUS: String contains
if (origin?.includes('example.com')) {
// Allows: https://evil.com?origin=example.com
}
// ✅ SECURE: Exact match or regex
const allowedOrigins = ['https://app.example.com'];
if (origin && allowedOrigins.includes(origin)) {
// Only allows exact matches
}3. Limit Exposed Headers
// ❌ Exposes all headers
res.setHeader('Access-Control-Expose-Headers', '*');
// ✅ Only expose necessary headers
res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count, X-Page-Number');4. Short Preflight Cache
// ❌ Very long cache (1 week)
res.setHeader('Access-Control-Max-Age', '604800');
// ✅ Reasonable cache (24 hours)
res.setHeader('Access-Control-Max-Age', '86400');Testing CORS
Manual Test
# Test GET request
curl -H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: X-Requested-With" \
-X OPTIONS \
https://api.example.com/data
# Check response headersAutomated Test
describe('CORS', () => {
it('should allow whitelisted origin', async () => {
const response = await fetch('/api/data', {
headers: { Origin: 'https://app.example.com' },
});
expect(response.headers.get('Access-Control-Allow-Origin'))
.toBe('https://app.example.com');
});
it('should block non-whitelisted origin', async () => {
const response = await fetch('/api/data', {
headers: { Origin: 'https://evil.com' },
});
expect(response.headers.get('Access-Control-Allow-Origin'))
.toBeNull();
});
});Common Issues
Issue: CORS Error Despite Correct Headers
Cause: Preflight request not handled
// ✅ Always handle OPTIONS
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}Issue: Credentials Not Sent
// Client side - must include credentials
fetch('https://api.example.com/data', {
credentials: 'include', // ← Important!
});
// Server side - must allow credentials
res.setHeader('Access-Control-Allow-Credentials', 'true');Common Pitfalls
❌ Wildcard with credentials: Security risk
✅ Specific origins with credentials
❌ Weak origin validation: includes() check
✅ Strict validation: exact match or regex
❌ No OPTIONS handler: Preflight fails
✅ Handle OPTIONS for all routes
❌ Exposing all headers: Information leak
✅ Expose only necessary headers
❌ Same CORS for all routes: Too permissive
✅ Different CORS per route sensitivity
CORS is about controlled access—be strict with allowed origins and never use wildcards with credentials!