Topic Overview

Cross-Origin Resource Sharing (CORS)

Master CORS: how browsers handle cross-origin requests, preflight requests, and security policies for web applications.

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that allows web pages to make requests to a different domain than the one serving the web page, while maintaining security.


What is CORS?

CORS enables:

  • Cross-origin requests: Requests from one origin to another
  • Controlled access: Server controls which origins can access resources
  • Security: Prevents unauthorized cross-origin access

Origin: Protocol + Domain + Port

https://example.com:443
http://localhost:3000

Same-origin: Same protocol, domain, and port Cross-origin: Different protocol, domain, or port


Same-Origin Policy

Same-Origin Policy restricts:

  • JavaScript: Can only access resources from same origin
  • AJAX requests: Blocked to different origins
  • Cookies: Not sent to different origins

Why needed:

  • Security: Prevents malicious websites from accessing user data
  • Privacy: Protects user information

Example:

Page: https://example.com
Request to: https://api.example.com
Result: Blocked (different origin)

CORS Headers

Server Response Headers

1. Access-Control-Allow-Origin

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Origin: *  (allow all origins)

2. Access-Control-Allow-Methods

Access-Control-Allow-Methods: GET, POST, PUT, DELETE

3. Access-Control-Allow-Headers

Access-Control-Allow-Headers: Content-Type, Authorization

4. Access-Control-Allow-Credentials

Access-Control-Allow-Credentials: true

5. Access-Control-Max-Age

Access-Control-Max-Age: 3600  (cache preflight for 1 hour)

Client Request Headers

1. Origin

Origin: https://example.com

2. Access-Control-Request-Method

Access-Control-Request-Method: POST

3. Access-Control-Request-Headers

Access-Control-Request-Headers: Content-Type

Simple vs Preflight Requests

Simple Requests

Conditions:

  • Method: GET, POST, or HEAD
  • Headers: Only simple headers (Accept, Content-Language, Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain)

Flow:

Browser → Server: GET /api/data
  Origin: https://example.com
  
Server → Browser: Response
  Access-Control-Allow-Origin: https://example.com
  Data: {...}

No preflight: Request sent directly

Preflight Requests

Triggered when:

  • Method: PUT, DELETE, PATCH, or custom methods
  • Headers: Custom headers (Authorization, Content-Type: application/json)

Flow:

Browser → Server: OPTIONS /api/data (preflight)
  Origin: https://example.com
  Access-Control-Request-Method: POST
  Access-Control-Request-Headers: Content-Type
  
Server → Browser: Preflight Response
  Access-Control-Allow-Origin: https://example.com
  Access-Control-Allow-Methods: POST
  Access-Control-Allow-Headers: Content-Type
  
Browser → Server: POST /api/data (actual request)
  Origin: https://example.com
  Content-Type: application/json
  
Server → Browser: Response
  Access-Control-Allow-Origin: https://example.com
  Data: {...}

Two requests: Preflight (OPTIONS) + Actual request


Examples

CORS Configuration (Express.js)

const express = require('express');
const cors = require('cors');
const app = express();

// Simple CORS (allow all)
app.use(cors());

// Custom CORS
app.use(cors({
  origin: 'https://example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 3600
}));

// Per-route CORS
app.get('/api/data', cors({
  origin: 'https://example.com'
}), (req, res) => {
  res.json({ data: 'success' });
});

Manual CORS Headers

app.use((req, res, next) => {
  const origin = req.headers.origin;
  
  // Check if origin is allowed
  const allowedOrigins = ['https://example.com', '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');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  
  // Handle preflight
  if (req.method === 'OPTIONS') {
    res.status(200).end();
    return;
  }
  
  next();
});

CORS with Credentials

// Client (browser)
fetch('https://api.example.com/data', {
  method: 'POST',
  credentials: 'include',  // Include cookies
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ data: 'value' })
});

// Server
app.use(cors({
  origin: 'https://example.com',
  credentials: true  // Allow credentials
}));

Common Pitfalls

  • Wildcard with credentials: Access-Control-Allow-Origin: * with credentials: true not allowed. Fix: Specify exact origin
  • Not handling preflight: OPTIONS request not handled. Fix: Handle OPTIONS requests, return appropriate headers
  • Missing headers: Not allowing required headers. Fix: Include all custom headers in Access-Control-Allow-Headers
  • CORS errors in console: Browser blocks request. Fix: Check server CORS configuration, verify origin
  • Not caching preflight: Preflight sent for every request. Fix: Set Access-Control-Max-Age

Interview Questions

Beginner

Q: What is CORS and why is it needed?

A:

CORS (Cross-Origin Resource Sharing) allows web pages to make requests to different domains while maintaining security.

Why needed:

  • Same-Origin Policy: Browsers block cross-origin requests by default
  • Security: Prevents malicious websites from accessing user data
  • Controlled access: Server controls which origins can access resources

How it works:

Page: https://example.com
Request to: https://api.example.com

Browser checks: Different origin (cross-origin)
Server responds: Access-Control-Allow-Origin: https://example.com
Browser allows: Request succeeds

Example:

Without CORS:
  Browser blocks: Cross-origin request denied

With CORS:
  Server allows: Access-Control-Allow-Origin: https://example.com
  Browser allows: Request succeeds

CORS Headers:

  • Access-Control-Allow-Origin: Which origins can access
  • Access-Control-Allow-Methods: Which methods allowed
  • Access-Control-Allow-Headers: Which headers allowed

Intermediate

Q: Explain the difference between simple and preflight requests. When is each used?

A:

Simple Requests:

Conditions:

  • Method: GET, POST, or HEAD
  • Headers: Only simple headers (Accept, Content-Language, Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain)

Flow:

Browser → Server: GET /api/data
  Origin: https://example.com
  
Server → Browser: Response
  Access-Control-Allow-Origin: https://example.com

No preflight: Request sent directly

Preflight Requests:

Triggered when:

  • Method: PUT, DELETE, PATCH, or custom methods
  • Headers: Custom headers (Authorization, Content-Type: application/json)

Flow:

1. Browser → Server: OPTIONS /api/data (preflight)
     Origin: https://example.com
     Access-Control-Request-Method: POST
     Access-Control-Request-Headers: Content-Type
   
2. Server → Browser: Preflight Response
     Access-Control-Allow-Origin: https://example.com
     Access-Control-Allow-Methods: POST
     Access-Control-Allow-Headers: Content-Type
   
3. Browser → Server: POST /api/data (actual request)
     Origin: https://example.com
     Content-Type: application/json
   
4. Server → Browser: Response
     Access-Control-Allow-Origin: https://example.com

Two requests: Preflight (OPTIONS) + Actual request

Why preflight:

  • Safety check: Browser checks if server allows request before sending
  • Protection: Prevents unauthorized requests

Senior

Q: Design a CORS system for a multi-tenant API that serves multiple client applications. How do you handle different origins, credentials, and security?

A:

class MultiTenantCORSSystem {
  private allowedOrigins: Map<string, string[]>; // tenant → origins
  private corsCache: Map<string, CORSConfig>;
  
  constructor() {
    this.allowedOrigins = new Map();
    this.corsCache = new Map();
  }
  
  // 1. Dynamic CORS Configuration
  async handleCORS(req: Request, res: Response): Promise<void> {
    const origin = req.headers.origin;
    const tenant = this.extractTenant(req);
    
    // Get allowed origins for tenant
    const allowed = this.getAllowedOrigins(tenant);
    
    if (allowed.includes(origin)) {
      // Set CORS headers
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
      res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Tenant-ID');
      res.setHeader('Access-Control-Allow-Credentials', 'true');
      res.setHeader('Access-Control-Max-Age', '3600');
    }
    
    // Handle preflight
    if (req.method === 'OPTIONS') {
      res.status(200).end();
      return;
    }
  }
  
  // 2. Tenant-Based Origin Management
  getAllowedOrigins(tenantId: string): string[] {
    // Get from database or cache
    if (!this.allowedOrigins.has(tenantId)) {
      const origins = this.loadOriginsFromDatabase(tenantId);
      this.allowedOrigins.set(tenantId, origins);
    }
    return this.allowedOrigins.get(tenantId);
  }
  
  // 3. CORS with Credentials
  handleCredentials(req: Request, res: Response): void {
    const origin = req.headers.origin;
    
    // Cannot use wildcard with credentials
    if (this.isAllowedOrigin(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin); // Specific origin
      res.setHeader('Access-Control-Allow-Credentials', 'true');
    }
  }
  
  // 4. Preflight Caching
  cachePreflight(origin: string, config: CORSConfig): void {
    const key = `${origin}:${config.methods}:${config.headers}`;
    this.corsCache.set(key, {
      ...config,
      cachedUntil: Date.now() + 3600000 // 1 hour
    });
  }
  
  // 5. Security: Origin Validation
  validateOrigin(origin: string): boolean {
    // Check against whitelist
    // Validate format
    // Check for malicious patterns
    return this.isValidOrigin(origin);
  }
  
  // 6. Rate Limiting for Preflight
  rateLimitPreflight(origin: string): boolean {
    // Prevent preflight abuse
    const count = this.getPreflightCount(origin);
    return count < 100; // Max 100 preflights per hour
  }
}

Features:

  1. Multi-tenant: Different CORS configs per tenant
  2. Dynamic origins: Load from database
  3. Credentials: Handle cookies/auth properly
  4. Preflight caching: Cache preflight responses
  5. Security: Origin validation, rate limiting

Key Takeaways

  • CORS: Browser security mechanism for cross-origin requests
  • Same-origin policy: Browsers block cross-origin requests by default
  • Simple requests: GET, POST, HEAD with simple headers (no preflight)
  • Preflight requests: OPTIONS request before actual request (for PUT, DELETE, custom headers)
  • CORS headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, etc.
  • Credentials: Cannot use wildcard (*) with credentials, must specify origin
  • Security: Validate origins, use whitelist, prevent abuse
  • Best practices: Handle preflight, cache preflight, specify exact origins with credentials

About the author

InterviewCrafted helps you master system design with patience. We believe in curiosity-led engineering, reflective writing, and designing systems that make future changes feel calm.