CORS Configuration and Security: Understanding Cross-Origin Resource Sharing

CORS Configuration and Security: Understanding Cross-Origin Resource Sharing

Whitespots Team ·
cors
web
api
http

Introduction

Cross-Origin Resource Sharing (CORS) is a security mechanism that controls how web applications from one origin can access resources from another origin. Misconfigured CORS policies are one of the most common web security vulnerabilities, potentially exposing sensitive data and APIs to unauthorized access. This guide covers CORS security with practical implementation examples.

Common CORS Security Issues

  1. Wildcard (*) origin with credentials
  2. Reflecting arbitrary origins
  3. Null origin acceptance
  4. Overly permissive allowed methods
  5. Missing preflight checks
  6. Accepting untrusted origins
  7. Exposing sensitive headers
  8. Missing origin validation

Understanding CORS

How CORS Works

plaintext
Client (https://app.example.com) | | 1. Preflight Request (OPTIONS) | Origin: https://app.example.com v Server (https://api.example.com) | | 2. Preflight Response | Access-Control-Allow-Origin: https://app.example.com | Access-Control-Allow-Methods: GET, POST | Access-Control-Allow-Credentials: true v Client | | 3. Actual Request (GET/POST) | Origin: https://app.example.com | Cookie: session=abc123 v Server | | 4. Actual Response | Access-Control-Allow-Origin: https://app.example.com | Access-Control-Allow-Credentials: true v Client (receives data)

Vulnerable CORS Configurations

Critical Vulnerability: Wildcard with Credentials

javascript
// VULNERABLE: Never do this! app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Credentials', 'true'); // FORBIDDEN! next(); }); // This configuration is invalid and browsers will block it // But attempting it shows misunderstanding of CORS

Vulnerable: Reflecting All Origins

javascript
// VULNERABLE: Accepts any origin app.use((req, res, next) => { const origin = req.headers.origin; res.setHeader('Access-Control-Allow-Origin', origin); // Dangerous! res.setHeader('Access-Control-Allow-Credentials', 'true'); next(); }); // Allows https://evil.com to access authenticated APIs

Vulnerable: Accepting Null Origin

javascript
// VULNERABLE: Null origin exploitation app.use((req, res, next) => { const origin = req.headers.origin; if (origin === 'null') { // Bad practice! res.setHeader('Access-Control-Allow-Origin', 'null'); res.setHeader('Access-Control-Allow-Credentials', 'true'); } next(); }); // Attackers can easily send requests with null origin // Example: <iframe sandbox src="data:text/html,...">

Secure CORS Configurations

javascript
// SECURE: Explicit origin whitelist const express = require('express'); const app = express(); const ALLOWED_ORIGINS = [ 'https://app.example.com', 'https://admin.example.com', 'https://mobile.example.com' ]; app.use((req, res, next) => { const origin = req.headers.origin; // Only allow whitelisted origins if (ALLOWED_ORIGINS.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Credentials', 'true'); 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'); // 24 hours } // Handle preflight if (req.method === 'OPTIONS') { return res.sendStatus(204); } next(); });

Using CORS Middleware

javascript
// Using cors package with secure config const cors = require('cors'); const corsOptions = { origin: function (origin, callback) { // Allow requests with no origin (mobile apps, Postman, etc.) if (!origin) { return callback(null, true); } const allowedOrigins = [ 'https://app.example.com', 'https://admin.example.com' ]; if (allowedOrigins.indexOf(origin) !== -1) { 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, optionsSuccessStatus: 204 }; app.use(cors(corsOptions));

Environment-Based Configuration

javascript
// Different CORS rules per environment const getOrigins = () => { switch (process.env.NODE_ENV) { case 'production': return [ 'https://app.example.com', 'https://admin.example.com' ]; case 'staging': return [ 'https://staging.example.com', 'https://staging-admin.example.com' ]; case 'development': return [ 'http://localhost:3000', 'http://localhost:3001' ]; default: return []; } }; const corsOptions = { origin: function (origin, callback) { const allowedOrigins = getOrigins(); if (!origin || allowedOrigins.indexOf(origin) !== -1) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true }; app.use(cors(corsOptions));

Pattern Matching for Subdomains

javascript
// Allow all subdomains of example.com const corsOptions = { origin: function (origin, callback) { if (!origin) { return callback(null, true); } // Match *.example.com and example.com const originPattern = /^https:\/\/([\w-]+\.)?example\.com$/; if (originPattern.test(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true }; app.use(cors(corsOptions));

Route-Specific CORS

javascript
// Different CORS policies for different routes const publicCors = cors({ origin: '*', credentials: false, methods: ['GET'] }); const authenticatedCors = cors({ origin: ['https://app.example.com'], credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE'] }); // Public endpoints app.get('/api/public/status', publicCors, (req, res) => { res.json({ status: 'ok' }); }); // Authenticated endpoints app.get('/api/user/profile', authenticatedCors, (req, res) => { // Requires authentication res.json({ user: req.user }); }); // No CORS for internal endpoints app.get('/internal/metrics', (req, res) => { res.json({ metrics: 'data' }); });

Preflight Request Handling

javascript
// Proper preflight handling app.options('*', (req, res) => { const origin = req.headers.origin; const allowedOrigins = ['https://app.example.com']; if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Max-Age', '86400'); res.sendStatus(204); } else { res.sendStatus(403); } });

CORS with Different Frameworks

Express.js

javascript
const express = require('express'); const cors = require('cors'); const app = express(); app.use(cors({ origin: 'https://app.example.com', credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'] }));

FastAPI (Python)

python
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() # SECURE CORS configuration app.add_middleware( CORSMiddleware, allow_origins=[ "https://app.example.com", "https://admin.example.com" ], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE"], allow_headers=["Content-Type", "Authorization"], max_age=86400 ) # DO NOT use allow_origins=["*"] with allow_credentials=True

Django

python
# settings.py CORS_ALLOWED_ORIGINS = [ "https://app.example.com", "https://admin.example.com", ] CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_METHODS = [ "GET", "POST", "PUT", "DELETE", "OPTIONS", ] CORS_ALLOW_HEADERS = [ "accept", "accept-encoding", "authorization", "content-type", "origin", "user-agent", "x-csrftoken", "x-requested-with", ] # Pattern matching for subdomains CORS_ALLOWED_ORIGIN_REGEXES = [ r"^https://\w+\.example\.com$", ]

Spring Boot (Java)

java
@Configuration public class CorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins( "https://app.example.com", "https://admin.example.com" ) .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("Content-Type", "Authorization") .allowCredentials(true) .maxAge(86400); } }; } }

Nginx

nginx
server { listen 443 ssl http2; server_name api.example.com; location /api { # Check origin set $cors ''; if ($http_origin ~* '^https://(app|admin)\.example\.com$') { set $cors 'true'; } # CORS headers if ($cors = 'true') { add_header 'Access-Control-Allow-Origin' "$http_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; } # Preflight requests if ($request_method = 'OPTIONS') { return 204; } proxy_pass http://backend; } }

Testing CORS Configuration

bash
# Test CORS with curl # Preflight request curl -X OPTIONS https://api.example.com/users \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Content-Type" \ -v # Actual request curl -X POST https://api.example.com/users \ -H "Origin: https://app.example.com" \ -H "Content-Type: application/json" \ -d '{"name":"John"}' \ -v

Automated Testing

javascript
// Jest test for CORS const request = require('supertest'); const app = require('../app'); describe('CORS Configuration', () => { const allowedOrigin = 'https://app.example.com'; const forbiddenOrigin = 'https://evil.com'; describe('Allowed Origin', () => { it('should allow preflight from whitelisted origin', async () => { const response = await request(app) .options('/api/users') .set('Origin', allowedOrigin) .set('Access-Control-Request-Method', 'POST'); expect(response.status).toBe(204); expect(response.headers['access-control-allow-origin']).toBe(allowedOrigin); expect(response.headers['access-control-allow-credentials']).toBe('true'); }); it('should allow actual request from whitelisted origin', async () => { const response = await request(app) .get('/api/users') .set('Origin', allowedOrigin); expect(response.status).toBe(200); expect(response.headers['access-control-allow-origin']).toBe(allowedOrigin); }); }); describe('Forbidden Origin', () => { it('should block preflight from non-whitelisted origin', async () => { const response = await request(app) .options('/api/users') .set('Origin', forbiddenOrigin) .set('Access-Control-Request-Method', 'POST'); expect(response.headers['access-control-allow-origin']).toBeUndefined(); }); it('should block actual request from non-whitelisted origin', async () => { const response = await request(app) .get('/api/users') .set('Origin', forbiddenOrigin); expect(response.headers['access-control-allow-origin']).toBeUndefined(); }); }); describe('Security', () => { it('should not allow wildcard with credentials', async () => { const response = await request(app) .get('/api/users') .set('Origin', allowedOrigin); const allowOrigin = response.headers['access-control-allow-origin']; const allowCreds = response.headers['access-control-allow-credentials']; if (allowCreds === 'true') { expect(allowOrigin).not.toBe('*'); } }); it('should not reflect arbitrary origins', async () => { const response = await request(app) .get('/api/users') .set('Origin', forbiddenOrigin); expect(response.headers['access-control-allow-origin']).not.toBe(forbiddenOrigin); }); }); });

CORS Security Checklist

  • ✅ Use explicit origin whitelist
  • ✅ Never use wildcard (*) with credentials
  • ✅ Don’t reflect arbitrary origins without validation
  • ✅ Reject null origin unless specifically needed
  • ✅ Limit allowed methods to minimum required
  • ✅ Restrict allowed headers
  • ✅ Set appropriate max-age for preflight caching
  • ✅ Use pattern matching carefully for subdomains
  • ✅ Different CORS policies for different routes
  • ✅ Test CORS configuration thoroughly
  • ✅ Log CORS violations
  • ✅ Review and update allowed origins regularly
  • ✅ Consider environment-specific configurations
  • ✅ Document CORS policy decisions

Common CORS Pitfalls

Pitfall 1: Development Shortcuts in Production

javascript
// BAD: Development config in production const corsOptions = { origin: process.env.NODE_ENV === 'development' ? '*' : allowedOrigins, credentials: true // Dangerous with wildcard! }; // GOOD: Separate configs const corsOptions = { origin: getAllowedOrigins(), // Returns appropriate list credentials: needsCredentials() };

Pitfall 2: Regex Mistakes

javascript
// BAD: Overly permissive regex const pattern = /example\.com/; // Matches evil-example.com.attacker.com // GOOD: Strict regex const pattern = /^https:\/\/([\w-]+\.)?example\.com$/;

Pitfall 3: Missing Preflight Handling

javascript
// BAD: Not handling OPTIONS app.post('/api/users', (req, res) => { // Will fail preflight }); // GOOD: Handle preflight app.options('/api/users', cors(corsOptions)); app.post('/api/users', cors(corsOptions), (req, res) => { // Works correctly });

Conclusion

CORS security requires careful configuration to balance functionality with security. By using explicit origin whitelists, avoiding wildcard credentials, and properly validating origins, you prevent unauthorized cross-origin access while enabling legitimate use cases.

CORS configurations should be regularly reviewed and tested as part of your security assessment process. For comprehensive API security reviews and CORS configuration audits, contact the Whitespots team for expert consultation.

Cookie Consent

Our website uses cookies to ensure the best user experience. Cookies help us to:

  • Authorize you

By clicking "Accept All Cookies", you consent to our use of cookies. You can also manage your preferences at any time by visiting our Cookie Settings page.

Learn More Manage Preferences