0

CORS — what it is, why browsers enforce it, how to configure it

Intermediate5 min read·eng-13-009
interviewsecurity

Concept

CORS (Cross-Origin Resource Sharing) — a browser security mechanism that restricts cross-origin HTTP requests made from JavaScript. "Origin" = scheme + host + port.

The Same-Origin Policy: Browsers block JavaScript on app.example.com from reading responses from api.example.com (different subdomain = different origin). This protects against malicious sites reading your private data.

CORS is a BROWSER feature: Curl, Postman, server-to-server requests — CORS doesn't apply. Only browser JavaScript is restricted.

Preflight request: For "non-simple" requests (non-GET/POST, or with custom headers like Authorization), the browser first sends an OPTIONS request to ask if the cross-origin request is allowed. The server responds with what it allows. If allowed, the browser sends the real request.

Simple request (no preflight): GET or POST with standard content types. Cookies still won't be sent cross-origin unless credentials: 'include' is set and the server returns Access-Control-Allow-Credentials: true.

Key CORS response headers:

  • Access-Control-Allow-Origin: * or https://app.example.com — which origins are allowed.
  • Access-Control-Allow-Methods: GET, POST, PUT, DELETE — which methods.
  • Access-Control-Allow-Headers: Authorization, Content-Type — which request headers.
  • Access-Control-Allow-Credentials: true — allow cookies/auth headers cross-origin.
  • Access-Control-Max-Age: 3600 — cache preflight result for 1 hour (reduces OPTIONS requests).

* and credentials: Access-Control-Allow-Origin: * cannot be used with Access-Control-Allow-Credentials: true. Must specify the exact origin.

Common mistake: CORS errors appear to be server errors, but they're browser enforcement. The server responded — the browser blocked reading the response.

Code Example

php
<?php
// WHAT CORS protects against
// You're on evil.com. This JS tries to read YOUR data from bank.com:
// fetch('https://bank.com/api/balance', {credentials: 'include'})
//   .then(r => r.json()).then(data => sendToAttacker(data));
// Browser blocks the response read — CORS prevents data theft!

// LARAVEL CORS configuration — config/cors.php
return [
    'paths'                    => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_methods'          => ['*'],                          // GET, POST, PUT, etc.
    'allowed_origins'          => ['https://app.example.com'],    // specific origin — NOT '*' if using credentials
    'allowed_origins_patterns' => [],
    'allowed_headers'          => ['Content-Type', 'X-Requested-With', 'Authorization'],
    'exposed_headers'          => ['X-RateLimit-Remaining'],      // headers JS can read
    'max_age'                  => 3600,                           // preflight cache
    'supports_credentials'     => true,                           // allow cookies
];

// Manual CORS middleware (for understanding)
class CorsMiddleware
{
    private array $allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];

    public function handle(Request $request, \Closure $next): mixed
    {
        $origin = $request->header('Origin');

        // Preflight request — browser asks "can I do this?"
        if ($request->isMethod('OPTIONS')) {
            if (!in_array($origin, $this->allowedOrigins)) {
                return response('', 403);
            }
            return response('', 204)
                ->header('Access-Control-Allow-Origin',      $origin)
                ->header('Access-Control-Allow-Methods',     'GET, POST, PUT, PATCH, DELETE, OPTIONS')
                ->header('Access-Control-Allow-Headers',     'Authorization, Content-Type, X-Request-Id')
                ->header('Access-Control-Allow-Credentials', 'true')
                ->header('Access-Control-Max-Age',           '3600');
        }

        $response = $next($request);

        // Add CORS headers to actual response too
        if (in_array($origin, $this->allowedOrigins)) {
            $response->headers->set('Access-Control-Allow-Origin',      $origin);
            $response->headers->set('Access-Control-Allow-Credentials', 'true');
        }

        return $response;
    }
}

// PREFLIGHT flow (what happens in the browser):
// 1. JS: fetch('https://api.example.com/users', {headers: {Authorization: 'Bearer ...'}})
// 2. Browser: OPTIONS https://api.example.com/users
//             Origin: https://app.example.com
//             Access-Control-Request-Method: GET
//             Access-Control-Request-Headers: authorization
// 3. Server: 204, Access-Control-Allow-Origin: https://app.example.com
//            Access-Control-Allow-Headers: authorization
// 4. Browser: GET https://api.example.com/users  ← real request, now allowed