This vulnerability occurs when a web application implements Cross-Origin Resource Sharing (CORS) policies that are too permissive, particularly by setting the Access-Control-Allow-Origin header to overly broad values like the wildcard (*) or by dynamically reflecting the requesting Origin header without proper validation. This allows malicious websites, visited by an authenticated user, to make requests to the vulnerable application and read the responses, potentially stealing sensitive user data or performing unauthorized actions via the user’s session. 🌐🔓➡️😈
Permissive CORS policies undermine the browser’s Same-Origin Policy, leading to:
Sensitive Data Exposure: Malicious websites can make authenticated requests (using the victim’s cookies) to the vulnerable application’s API endpoints and read sensitive data returned in the response (e.g., user profile details, messages, financial information).
Unauthorized Actions: While the primary risk is data reading, permissive CORS can sometimes facilitate actions if combined with other vulnerabilities or if the endpoint relies solely on cookies for authorization and performs state changes via GET requests (though this is less common).
Trust Exploitation: It abuses the trust relationship between the user and the vulnerable application.
Reference Details
CWE ID:CWE-942 (Related: CWE-346 Origin Validation Error, CWE-264 Permissions/Privileges)
OWASP Top 10 (2021): A05:2021 - Security Misconfiguration
Severity: High (especially if sensitive data is exposed)
CORS is typically configured either at the web server/proxy level (Nginx, Apache) or within the application framework using middleware or filters. The vulnerability is allowing origins that should not be trusted.Key Remediation Principles:
Avoid Wildcard (*) if Credentials Allowed:Never set Access-Control-Allow-Origin: * if Access-Control-Allow-Credentials: true is also set. Browsers generally block this combination anyway, but relying on it is insecure.
Use Strict Allow-lists: Maintain an explicit list of trusted origin domains that are permitted to make cross-origin requests.
Validate Dynamic Origins: If dynamically reflecting the Origin header, strictly validate it against the allow-list. Do not simply echo back any origin.
Least Privilege: Only allow the specific HTTP methods (Access-Control-Allow-Methods) and headers (Access-Control-Allow-Headers) required by the trusted origins.
Vary Header: Consider using the Vary: Origin header to prevent caching issues when serving different Access-Control-Allow-Origin headers based on the request origin.
# settings.py (Django)INSTALLED_APPS = [ ..., 'corsheaders', ... ]MIDDLEWARE = [ ..., 'corsheaders.middleware.CorsMiddleware', ... ]# DANGEROUS: Allows any origin to make requests.# If ALLOW_CREDENTIALS is True (or default), sensitive data can be read.CORS_ALLOW_ALL_ORIGINS = True# CORS_ALLOW_CREDENTIALS = True # Defaults to False, but True makes '*' deadly
Django: Set CORS_ALLOW_ALL_ORIGINS = False. Define CORS_ALLOWED_ORIGINS (list of specific domains like https://trusted.example.com) or CORS_ALLOWED_ORIGIN_REGEXES. Set CORS_ALLOW_CREDENTIALS = True only if necessary and origins are strictly controlled.
Flask: Replace "origins": "*" with a specific list: "origins": ["https://trusted.example.com", "https://another.trusted.com"]. Only set supports_credentials=True if absolutely needed and origins are restricted.
Use curl or browser developer tools to send requests to your API endpoints from a different origin (e.g., set the Origin header manually in curl).
Check Access-Control-Allow-Origin. Is it *? Is it reflecting an untrusted origin?
Check Access-Control-Allow-Credentials. Is it true?
If both Origin: * and Credentials: true are returned (or if an untrusted origin is reflected with Credentials: true), the configuration is vulnerable. Test by making a JavaScript fetch request from a dummy HTML page served on a different domain to see if you can read authenticated data.
Replace allowedOrigins("*") or @CrossOrigin(origins = "*") with specific, trusted origins: allowedOrigins("https://trusted.com") or @CrossOrigin(origins = "https://trusted.com"). Only set allowCredentials(true) / allowCredentials = "true" when strictly necessary and origins are limited.
// controller/UserController.java (Secure Annotation)// SECURE: Specify allowed origins explicitly.@CrossOrigin(origins = "[https://trusted.frontend.com](https://trusted.frontend.com)", allowCredentials = "true")@GetMapping("/api/user/details")public UserDetails getUserDetails(Principal principal) { // ...}// config/WebConfig.java (Secure Global Config)@Configurationpublic class WebConfig { @Value("${app.cors.allowed-origins}") // Load from properties/env private String[] allowedOrigins; @Bean public WebMvcConfigurer corsConfigurerSecure() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { // SECURE: Use allow-list loaded from configuration. registry.addMapping("/api/**") .allowedOrigins(allowedOrigins) // Use the list .allowCredentials(true) // Only if needed .allowedMethods("GET", "POST", "PUT", "DELETE"); // Be specific } }; }}
# application.properties (Secure)# Comma-separated list of trusted originsapp.cors.allowed-origins=[https://trusted.frontend.com](https://trusted.frontend.com),[https://partner.com](https://partner.com)
Send requests with different Origin headers (trusted, untrusted, null) to API endpoints requiring authentication. Examine the Access-Control-Allow-Origin and Access-Control-Allow-Credentials response headers. Use browser-based tests (JavaScript fetch from another domain) to confirm if authenticated data can be read from untrusted origins when allowCredentials is true.
Vulnerable Scenario 2: Reflecting Origin Without Validation
Building a custom CORS middleware or logic that incorrectly reflects the Origin header.
// Custom Middleware (Conceptual)public async Task InvokeAsync(HttpContext context){ var origin = context.Request.Headers["Origin"].FirstOrDefault(); if (!string.IsNullOrEmpty(origin)) { // DANGEROUS: Reflecting any origin back without checking an allow-list. context.Response.Headers.Add("Access-Control-Allow-Origin", origin); context.Response.Headers.Add("Access-Control-Allow-Credentials", "true"); // Also dangerous } await _next(context);}
Define CORS policies with specific origins using WithOrigins("https://trusted.com", ...). Never use AllowAnyOrigin() together with AllowCredentials(). If dynamic origin validation is needed, implement logic to check the incoming Origin header against a configured allow-list before setting the Access-Control-Allow-Origin header.
Send requests with various Origin headers (trusted, untrusted, null). Inspect Access-Control-Allow-Origin and Access-Control-Allow-Credentials. Use browser JS (fetch) from an untrusted domain to attempt reading data from authenticated endpoints. Verify that only requests from origins listed in the policy succeed when AllowCredentials is true.
Laravel: Configure config/cors.php. Set allowed_origins to an explicit array of trusted domains. Set supports_credentials to true only if needed and origins are restricted. Avoid ['*'] for origins if credentials are true.
Manual: Maintain an allow-list of trusted origins. Check the incoming $_SERVER['HTTP_ORIGIN'] against this list. If it matches, set Access-Control-Allow-Origin to that specific origin. Only set Access-Control-Allow-Credentials: true for matched, trusted origins.
Use curl with -H "Origin: <domain>" (testing trusted, untrusted, and null origins) against authenticated endpoints. Check the Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers. Use browser JS (fetch) from an untrusted domain to attempt reading authenticated data. Verify only allow-listed origins succeed when credentials is true.
Vulnerable Scenario 1: cors() Allowing All Origins with Credentials
// app.jsconst express = require('express');const cors = require('cors');const app = express();// DANGEROUS: Allows any origin and allows credentials.const corsOptions = { origin: '*', // Allows any origin credentials: true // Allows cookies/auth headers};app.use(cors(corsOptions));app.get('/api/sensitive-data', ensureAuthenticated, (req, res) => { // Attacker on evil.com makes request using victim's cookie, // CORS allows the response with sensitive data to be read. res.json({ secret: getSecretData(req.user.id) });});
Configure the cors middleware with a specific list of allowed origins. Never use origin: '*' with credentials: true. If using a function for origin, validate the incoming origin against your allow-list.
// app.js (Secure)const express = require('express');const cors = require('cors');const app = express();// SECURE: Load allowed origins from environment/configconst allowedOrigins = process.env.CORS_ALLOWED_ORIGINS ? process.env.CORS_ALLOWED_ORIGINS.split(',') : [];if (allowedOrigins.length === 0) { console.warn("CORS allowed origins not set!"); }const secureCorsOptions = { // origin: allowedOrigins, // Option 1: Provide the array directly // Option 2: Use a function for more control or complex validation origin: function (origin, callback) { // Allow requests with no origin (like mobile apps or curl requests) // OR check if origin is in our allow-list if (!origin || allowedOrigins.indexOf(origin) !== -1) { callback(null, true); // Allow } else { callback(new Error('Not allowed by CORS')); // Disallow } }, credentials: true // Allow credentials only for the matched origins};// Apply CORS globally or selectively before protected routesapp.use(cors(secureCorsOptions));// Alternatively: app.use('/api/', cors(secureCorsOptions));app.get('/api/sensitive-data', ensureAuthenticated, (req, res) => { res.json({ secret: getSecretData(req.user.id) });});
Use curl with different Origin headers (trusted, untrusted) against authenticated endpoints. Check Access-Control-Allow-Origin (should match trusted origin or be absent for untrusted) and Access-Control-Allow-Credentials (should be true only for trusted origin). Use browser JS (fetch) from an untrusted domain to confirm data cannot be read.
Configure rack-cors in config/initializers/cors.rb to specify an explicit list of trusted origins. Avoid origins '*'. Only set credentials: true if required and paired with the explicit origin list.
Use curl with different Origin headers (trusted, untrusted) against authenticated API endpoints. Check Access-Control-Allow-Origin and Access-Control-Allow-Credentials. Use browser JS (fetch) from an untrusted domain to try reading authenticated data. Verify only origins in the configuration allow credentialed requests.