Cross-Origin Resource Sharing (CORS) controls which websites can access your API. Misconfiguration leads to security vulnerabilities or broken applications.
How CORS Works#
Same-Origin Policy:
- Browser security feature
- Blocks cross-origin requests by default
- Origin = protocol + domain + port
CORS:
- Server opts in to cross-origin requests
- Uses HTTP headers
- Browser enforces the policy
Example Origins:
https://example.com - different from:
https://api.example.com - different subdomain
http://example.com - different protocol
https://example.com:8080 - different port
Simple vs Preflight Requests#
Simple Requests (no preflight):
- GET, HEAD, POST
- Only simple headers (Accept, Content-Type, etc.)
- Content-Type: text/plain, multipart/form-data, application/x-www-form-urlencoded
Preflight Required:
- PUT, DELETE, PATCH
- Custom headers
- Content-Type: application/json
- Credentials (cookies)
Preflight Flow:
1. Browser sends OPTIONS request
2. Server responds with allowed origins/methods
3. Browser sends actual request
4. Server processes request
CORS Headers#
Response Headers:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Access-Control-Expose-Headers: X-Custom-Header
Request Headers (set by browser):
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Express CORS Configuration#
1import cors from 'cors';
2import express from 'express';
3
4const app = express();
5
6// Basic - allow all origins (NOT for production)
7app.use(cors());
8
9// Production configuration
10const corsOptions: cors.CorsOptions = {
11 origin: (origin, callback) => {
12 const allowedOrigins = [
13 'https://myapp.com',
14 'https://www.myapp.com',
15 'https://admin.myapp.com',
16 ];
17
18 // Allow requests with no origin (mobile apps, curl, etc.)
19 if (!origin) {
20 return callback(null, true);
21 }
22
23 if (allowedOrigins.includes(origin)) {
24 callback(null, true);
25 } else {
26 callback(new Error('Not allowed by CORS'));
27 }
28 },
29 methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
30 allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
31 exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
32 credentials: true,
33 maxAge: 86400, // 24 hours
34};
35
36app.use(cors(corsOptions));
37
38// Per-route configuration
39app.get('/public', cors({ origin: '*' }), (req, res) => {
40 res.json({ message: 'Public endpoint' });
41});
42
43app.get('/private', cors(corsOptions), (req, res) => {
44 res.json({ message: 'Private endpoint' });
45});Dynamic Origin Validation#
1// Validate against database or pattern
2const corsOptions: cors.CorsOptions = {
3 origin: async (origin, callback) => {
4 if (!origin) {
5 return callback(null, true);
6 }
7
8 // Check against pattern
9 const isAllowed = /^https:\/\/.*\.myapp\.com$/.test(origin);
10
11 if (isAllowed) {
12 callback(null, true);
13 return;
14 }
15
16 // Check database for allowed origins
17 const tenant = await db.tenant.findFirst({
18 where: { customDomain: new URL(origin).hostname },
19 });
20
21 if (tenant) {
22 callback(null, true);
23 } else {
24 callback(new Error('Not allowed by CORS'));
25 }
26 },
27 credentials: true,
28};Manual CORS Implementation#
1// Without cors middleware
2function corsMiddleware(
3 req: Request,
4 res: Response,
5 next: NextFunction
6): void {
7 const origin = req.headers.origin;
8 const allowedOrigins = ['https://myapp.com'];
9
10 if (origin && allowedOrigins.includes(origin)) {
11 res.setHeader('Access-Control-Allow-Origin', origin);
12 res.setHeader('Access-Control-Allow-Credentials', 'true');
13 }
14
15 if (req.method === 'OPTIONS') {
16 res.setHeader(
17 'Access-Control-Allow-Methods',
18 'GET, POST, PUT, DELETE, PATCH'
19 );
20 res.setHeader(
21 'Access-Control-Allow-Headers',
22 'Content-Type, Authorization'
23 );
24 res.setHeader('Access-Control-Max-Age', '86400');
25 res.status(204).end();
26 return;
27 }
28
29 next();
30}
31
32app.use(corsMiddleware);Next.js API Routes#
1// pages/api/users.ts
2import type { NextApiRequest, NextApiResponse } from 'next';
3import Cors from 'cors';
4
5const cors = Cors({
6 methods: ['GET', 'POST'],
7 origin: ['https://myapp.com'],
8});
9
10function runMiddleware(
11 req: NextApiRequest,
12 res: NextApiResponse,
13 fn: Function
14): Promise<void> {
15 return new Promise((resolve, reject) => {
16 fn(req, res, (result: any) => {
17 if (result instanceof Error) {
18 return reject(result);
19 }
20 return resolve(result);
21 });
22 });
23}
24
25export default async function handler(
26 req: NextApiRequest,
27 res: NextApiResponse
28) {
29 await runMiddleware(req, res, cors);
30
31 res.json({ users: [] });
32}
33
34// next.config.js for static CORS headers
35module.exports = {
36 async headers() {
37 return [
38 {
39 source: '/api/:path*',
40 headers: [
41 { key: 'Access-Control-Allow-Origin', value: 'https://myapp.com' },
42 { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE' },
43 { key: 'Access-Control-Allow-Headers', value: 'Content-Type' },
44 ],
45 },
46 ];
47 },
48};Common Pitfalls#
1// ❌ DANGEROUS: Reflecting origin without validation
2app.use((req, res, next) => {
3 res.setHeader('Access-Control-Allow-Origin', req.headers.origin!);
4 next();
5});
6
7// ❌ DANGEROUS: Wildcard with credentials
8app.use(cors({
9 origin: '*',
10 credentials: true, // This won't work and is insecure if it did
11}));
12
13// ❌ BAD: Overly permissive
14app.use(cors({
15 origin: true, // Allows any origin
16 credentials: true,
17}));
18
19// ✓ GOOD: Explicit allowed origins
20app.use(cors({
21 origin: ['https://myapp.com', 'https://admin.myapp.com'],
22 credentials: true,
23}));
24
25// ✓ GOOD: Validate subdomain pattern
26app.use(cors({
27 origin: (origin, callback) => {
28 if (!origin || /^https:\/\/[\w-]+\.myapp\.com$/.test(origin)) {
29 callback(null, true);
30 } else {
31 callback(new Error('Not allowed'));
32 }
33 },
34}));Debugging CORS#
1// Log CORS issues
2app.use((req, res, next) => {
3 console.log({
4 origin: req.headers.origin,
5 method: req.method,
6 path: req.path,
7 });
8 next();
9});
10
11// Browser console
12// Look for:
13// - "No 'Access-Control-Allow-Origin' header"
14// - "Preflight response is not successful"
15// - "Credentials flag is true, but Access-Control-Allow-Credentials is not 'true'"
16
17// Check preflight
18curl -X OPTIONS https://api.example.com/users \
19 -H "Origin: https://myapp.com" \
20 -H "Access-Control-Request-Method: POST" \
21 -H "Access-Control-Request-Headers: Content-Type" \
22 -vSecurity Checklist#
Configuration:
✓ Explicitly list allowed origins
✓ Never use wildcard with credentials
✓ Validate dynamic origins carefully
✓ Set appropriate Max-Age
Headers:
✓ Only expose necessary headers
✓ Only allow necessary methods
✓ Validate Content-Type for POST/PUT
✓ Consider Access-Control-Max-Age
Monitoring:
✓ Log blocked CORS requests
✓ Alert on unexpected origins
✓ Review allowed origins regularly
✓ Test CORS in staging
Best Practices#
Development:
- Use proxy in development to avoid CORS
- Test with production CORS settings
- Don't disable CORS for convenience
Production:
- Whitelist specific origins
- Use HTTPS only origins
- Regular security audits
- Monitor for abuse
Conclusion#
CORS protects your API from unauthorized cross-origin access. Always explicitly list allowed origins, never reflect arbitrary origins, and test thoroughly. When credentials are involved, be especially careful—wildcard origins won't work and reflecting origins is dangerous.