CORS — what it is, why browsers enforce it, how to configure it
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: *orhttps://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
// 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