Back to Blog
CORSSecurityWebAPI

CORS Security: A Complete Guide

Understand and configure CORS correctly. From browser security to configuration to common pitfalls.

B
Bootspring Team
Engineering
October 20, 2022
5 min read

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 -v

Security 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.

Share this article

Help spread the word about Bootspring