The Same-Origin Policy: Why It Exists
The Same-Origin Policy (SOP) is the most important security boundary in web browsers. It dictates that JavaScript code on one origin (defined by protocol + domain + port) cannot read responses from requests to a different origin. This prevents a fundamental attack scenario: a malicious website using your browser โ and your authenticated session cookies โ to read your email, bank balance, private messages, or any other data from services you're logged into.
Without SOP, visiting any malicious website would give its JavaScript code the ability to silently read all your data from every other service you're authenticated with โ your complete email history, financial account balances, private social media posts, corporate systems, everything. SOP makes this attack impossible by default.
But legitimate applications need to make cross-origin requests. A React frontend on app.company.com needs to call APIs on api.company.com. A JavaScript widget embedded on partner sites needs to communicate with the originating service. CORS (Cross-Origin Resource Sharing) is the standardized mechanism for selectively relaxing SOP in controlled, declared ways โ allowing specific trusted origins access while maintaining the security boundary against untrusted origins.
The Anatomy of a CORS Request
Understanding how CORS actually works is prerequisite to understanding how it fails:
- A browser script on
frontend.commakes a request toapi.backend.comโ a cross-origin request - For "simple" requests (GET/POST with standard content types), the browser sends the request immediately and includes an
Origin: https://frontend.comheader - For "non-simple" requests (custom headers, PUT/DELETE, JSON content-type), the browser sends a preflight OPTIONS request first, asking the server whether the actual request is permitted
- The server responds with CORS headers declaring which origins are allowed, which methods are permitted, and which headers can be used
- If the server's CORS response permits the requesting origin, the browser allows the JavaScript to read the response
- If the server doesn't allow the requesting origin, the browser blocks JavaScript from reading the response โ even if the request itself was sent and processed by the server
This is a critical security point: CORS controls whether JavaScript can read responses, not whether requests are sent. A CORS misconfiguration that allows response reading to an unauthorized origin is the vulnerability โ not the request itself.
The Wildcard Origin: When Access-Control-Allow-Origin: * Is Dangerous
The wildcard Access-Control-Allow-Origin: * tells the browser that any origin can read responses from this server. For truly public APIs returning non-sensitive, non-personalized data (weather information, public transit schedules, static content), wildcard CORS is acceptable and convenient โ it allows easy consumption of the API from any origin without configuration.
For authenticated APIs โ any API that returns user-specific data, accepts authentication tokens, or performs actions on behalf of authenticated users โ wildcard CORS is a critical vulnerability. Here's why it remains dangerous even when the spec prohibits combining wildcard with credentials:
- Developers sometimes set
Access-Control-Allow-Origin: *alongsideAccess-Control-Allow-Credentials: truethrough misconfiguration or misunderstanding the spec - Some server frameworks incorrectly implement CORS and don't enforce the spec's prohibition on this combination
- Token-based authentication (where the token is passed in an Authorization header rather than a cookie) can be combined with wildcard CORS in ways that don't violate the spec but still create security risks
Origin Reflection: The Subtle Misconfiguration
Origin reflection is arguably more dangerous than explicit wildcard because it's less obvious and appears to be "working correctly." The misconfigured server reads the Origin header from incoming requests and echoes it back in the Access-Control-Allow-Origin response without validation:
- Attacker creates a page on
evil.com - The attacker's page makes a cross-origin authenticated request to
api.victim.com, including the victim's session cookie - The server receives
Origin: https://evil.comand responds withAccess-Control-Allow-Origin: https://evil.comโ reflecting the attacker-controlled origin - The browser sees the CORS header allowing
evil.comand allows the attacker's script to read the full response, including sensitive user data
The fix is maintaining an explicit whitelist of allowed origins and only reflecting origins that appear in that whitelist. Reflecting arbitrary origins is functionally equivalent to a wildcard.
Common CORS Misconfigurations and Their Exploitability
A comprehensive CORS misconfiguration catalog:
- Trusting null origin:
Access-Control-Allow-Origin: nullโ an attacker can create a sandboxed iframe that sends requests withOrigin: null, bypassing origin-based restrictions - Prefix/suffix matching flaws: Attempting to validate origins with string matching rather than exact comparison.
origin.startsWith("https://company.com")would be bypassed byhttps://company.com.attacker.com - Regex matching flaws:
^https://.*.company.com$should work correctly;.*company.com.*would be bypassed byhttps://notcompany.comwhere the period isn't escaped - Missing Vary: Origin header: When CORS policy varies by origin, responses must include
Vary: Originto prevent CDN and proxy caches from serving one user's CORS-allowed response to another user from a different origin - HTTP origins trusted when HTTPS is used: If your application allows
http://yourfrontend.comas a CORS origin, an attacker who can perform a man-in-the-middle attack on the HTTP connection can modify the page to make malicious cross-origin requests
Correct CORS Implementation
The secure CORS implementation pattern:
- Maintain an explicit allowlist of approved origins. Store it in configuration, not hardcoded in route handlers.
- Validate exactly: Compare the incoming
Originheader against the allowlist using exact string matching (case-insensitive). Never use prefix matching or regex unless you've carefully tested for bypass. - Return the matched origin, not a wildcard: If the origin is in your allowlist, return
Access-Control-Allow-Origin: [that specific origin]. - Always return Vary: Origin when your CORS policy varies by origin.
- Handle preflight correctly: Respond to OPTIONS requests with the appropriate CORS headers and a 204 No Content or 200 OK status. Include
Access-Control-Max-Ageto cache preflight results and reduce preflight overhead. - Restrict allowed methods and headers: Only include methods and headers in
Access-Control-Allow-MethodsandAccess-Control-Allow-Headersthat your API actually uses. - Test from multiple unauthorized origins to verify that access is correctly denied. Security testing should include attempts to exploit all common bypass patterns.
Testing CORS Configuration Thoroughly
CORS misconfigurations are notoriously easy to miss in standard security testing because they require testing from the attacker's perspective โ making requests from unauthorized origins and verifying that access is correctly denied. A comprehensive CORS security test should verify: that requests from unlisted origins receive a 403 or no CORS headers (not a matching CORS header); that the null origin is not trusted; that origin header values with your domain as a substring but different actual domain are rejected; that HTTP origins are not trusted when your policy requires HTTPS; and that preflight responses correctly restrict allowed methods and headers. Automated CORS policy analyzers perform this systematic testing without requiring manual construction of test requests from each potential bypass angle.
CORS in Multi-Tenant and Microservices Architectures
CORS configuration complexity increases significantly in microservices architectures where multiple backend services with different CORS requirements serve a single frontend application. A common mistake is configuring overly broad CORS policies on internal services that are intended to be called only from specific trusted origins, because "they're only accessible internally." Network-level internal access controls and application-level CORS validation should both be present โ defense in depth ensures that a misconfigured network policy doesn't expose internal services to cross-origin attacks. Each service in a microservices architecture should have its own tightly configured CORS policy reflecting its specific trust requirements, rather than inheriting a broad policy from a shared API gateway configuration.