ðŸĶ–DinosaurBrowserSecurityCORSNetworking

CORS Deep Dive

CORS isn't a security feature you bolt on — it's the browser's enforcement of the same-origin policy for cross-origin requests. Understanding preflight, credentials, and header negotiation prevents the most common deployment bugs.

CORS Deep Dive

Cross-Origin Resource Sharing is the most commonly misunderstood web security mechanism. Developers encounter CORS errors, add Access-Control-Allow-Origin: * to their server, and call it done. Senior engineers understand why CORS exists, how the preflight negotiation works, and when * is dangerously wrong.

The Same-Origin Policy

Two URLs have the same origin if they share the same scheme, host, and port:

https://app.example.com:443/page   ← origin
  ↓         ↓              ↓
scheme    host            port
 
Same origin:     https://app.example.com/other-page
Different origin: http://app.example.com     (different scheme)
Different origin: https://api.example.com    (different host)
Different origin: https://app.example.com:8080 (different port)

Without the same-origin policy, any page could read your authenticated data from other sites. CORS is the controlled relaxation of this restriction.

Simple Requests vs Preflight

Simple Requests (No Preflight)

A request skips preflight if it meets all conditions:

  • Method: GET, HEAD, or POST
  • Headers: Only safe-listed headers (Accept, Accept-Language, Content-Language, Content-Type with specific values)
  • Content-Type: Only application/x-www-form-urlencoded, multipart/form-data, or text/plain
Browser → Server: GET /api/data
                  Origin: https://app.example.com
 
Server → Browser: 200 OK
                  Access-Control-Allow-Origin: https://app.example.com
                  Content-Type: application/json
                  { data: ... }

The browser makes the request directly. If the response lacks the correct Access-Control-Allow-Origin, the browser blocks JavaScript from reading the response — the request still reached the server.

Preflight Requests

Any request that doesn't qualify as "simple" triggers a preflight:

Browser → Server: OPTIONS /api/data
                  Origin: https://app.example.com
                  Access-Control-Request-Method: PUT
                  Access-Control-Request-Headers: Content-Type, Authorization
 
Server → Browser: 204 No Content
                  Access-Control-Allow-Origin: https://app.example.com
                  Access-Control-Allow-Methods: GET, PUT, DELETE
                  Access-Control-Allow-Headers: Content-Type, Authorization
                  Access-Control-Max-Age: 86400
 
[If preflight succeeds]
Browser → Server: PUT /api/data
                  Origin: https://app.example.com
                  Authorization: Bearer token123
                  Content-Type: application/json

Common triggers for preflight:

  • Content-Type: application/json
  • Custom headers (Authorization, X-Request-ID)
  • Methods other than GET/HEAD/POST
  • ReadableStream body

Preflight Caching

Access-Control-Max-Age tells the browser how long (in seconds) to cache the preflight response:

Access-Control-Max-Age: 86400    // Cache for 24 hours

Without this, every non-simple request generates two HTTP requests.

Credentials and Cookies

By default, cross-origin requests don't include cookies. To send credentials:

Client:

fetch('https://api.example.com/data', {
  credentials: 'include',
});

Server must respond with:

Access-Control-Allow-Origin: https://app.example.com  (NOT *)
Access-Control-Allow-Credentials: true

When credentials are involved, Access-Control-Allow-Origin: * is forbidden. The server must echo the specific origin. This prevents credential leakage to arbitrary origins.

CORS Headers Reference

Response Headers (Server → Browser)

HeaderPurpose
Access-Control-Allow-OriginWhich origins can read the response
Access-Control-Allow-MethodsAllowed HTTP methods
Access-Control-Allow-HeadersAllowed request headers
Access-Control-Expose-HeadersResponse headers readable by JS
Access-Control-Max-AgePreflight cache duration (seconds)
Access-Control-Allow-CredentialsAllow cookies/auth headers

Request Headers (Browser → Server, auto-set)

HeaderPurpose
OriginThe requesting origin
Access-Control-Request-MethodMethod for actual request (preflight)
Access-Control-Request-HeadersHeaders for actual request (preflight)

Common CORS Patterns

API Gateway Configuration

const corsOptions = {
  origin: (origin, callback) => {
    const allowlist = [
      'https://app.example.com',
      'https://staging.example.com',
    ];
    if (!origin || allowlist.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count', 'X-Request-ID'],
  maxAge: 86400,
};

Proxy Pattern (Dev and Production)

// next.config.ts — API proxy to avoid CORS in development
export default {
  async rewrites() {
    return [
      { source: '/api/:path*', destination: 'https://api.example.com/:path*' },
    ];
  },
};

In production, same-origin API proxies eliminate CORS entirely.

Debugging CORS Issues

  1. Check the Network tab — Look for the OPTIONS request, verify response headers
  2. The error is always in the response — CORS errors mean the server's response headers are wrong, not the client's request
  3. Origin null — Requests from file://, data: URIs, or sandboxed iframes send Origin: null. Handle explicitly
  4. Redirect after preflight — A redirect on the preflight OPTIONS response will fail. The actual resource URL must respond to OPTIONS directly

Interview Signal

Senior candidates demonstrate:

  1. Preflight mechanics — When and why OPTIONS fires, what triggers it, caching with Max-Age
  2. Credentials nuance — * is invalid with credentials, specific origin echoing requirement
  3. Security understanding — CORS protects the user (browser enforcement), not the server. Server still receives the request
  4. Debugging fluency — Network tab inspection, understanding that the error is always server-side headers
  5. Architecture — Same-origin proxy patterns that eliminate CORS complexity entirely