SyntaxStudy
Sign Up
Web Security Configuring Access-Control Headers Securely
Web Security Beginner 1 min read

Configuring Access-Control Headers Securely

The most dangerous CORS misconfiguration is setting `Access-Control-Allow-Origin: *` while also allowing credentials. The browser explicitly forbids this combination, but some servers work around it by dynamically echoing the request's `Origin` header back as the allowed origin without validating it against an allowlist. This effectively disables SOP for the endpoint, allowing any malicious website to make authenticated requests on behalf of the victim. A secure CORS configuration maintains an explicit allowlist of trusted origins and validates the incoming `Origin` header against it. If the origin is in the list, set `Access-Control-Allow-Origin` to that specific origin (not `*`) and add `Vary: Origin` to the response so caches serve the correct version to different origins. Origins not in the allowlist should receive no CORS headers — the browser will block the cross-origin read. Additional CORS headers to configure carefully: `Access-Control-Allow-Methods` should list only the methods your API actually uses; do not include `DELETE` on a read-only endpoint. `Access-Control-Allow-Headers` should list only the custom headers your API accepts. `Access-Control-Max-Age` caches the preflight response for the specified seconds, reducing OPTIONS round trips — 86400 (one day) is a reasonable value for stable APIs. `Access-Control-Expose-Headers` controls which response headers JavaScript can read; by default only "safe" headers are accessible.
Example
<?php
// Laravel: secure CORS configuration via fruitcake/laravel-cors (included since L8)

// config/cors.php
return [
    'paths'                => ['api/*', 'sanctum/csrf-cookie'],

    // SECURE: explicit allowlist — never use '*' with credentials
    'allowed_origins'      => [
        'https://app.example.com',
        'https://admin.example.com',
    ],

    // Dynamic origin validation (more flexible for multi-tenant apps)
    'allowed_origins_patterns' => [
        // '/^https:\/\/[\w-]+\.example\.com$/',  // any subdomain of example.com
    ],

    'allowed_methods'      => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
    'allowed_headers'      => ['Content-Type', 'Authorization', 'X-Requested-With'],
    'exposed_headers'      => ['X-RateLimit-Remaining', 'X-RateLimit-Limit'],
    'max_age'              => 86400,
    'supports_credentials' => true,  // Required for cookie-based auth; disallows '*' origin
];

// Middleware stack in bootstrap/app.php:
// ->withMiddleware(function (Middleware $middleware) {
//     $middleware->api(prepend: [
//         \Illuminate\Http\Middleware\HandleCors::class,
//     ]);
// })

// Manual CORS headers (for reference / non-Laravel contexts):
// header('Access-Control-Allow-Origin: https://app.example.com');
// header('Access-Control-Allow-Credentials: true');
// header('Vary: Origin');
// header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
// header('Access-Control-Allow-Headers: Content-Type, Authorization');
// header('Access-Control-Max-Age: 86400');