CORS Misconfigurations That Silently Expose Your API to Any Origin
You set up CORS headers so your frontend can talk to your API, tested that requests work, and moved on. That's exactly how most CORS misconfigurations slip into production β they look correct until an attacker reads your users' private data from a page they control.
CORS mistakes are different from most vulnerabilities: they don't generate errors in your logs, they don't crash anything, and they often pass code review because the intent seems reasonable. Understanding the precise patterns that create exposure is the only way to catch them.
What CORS Is Actually Doing (and What It Is Not)
Browsers enforce the Same-Origin Policy (SOP): a script running on https://evil.com cannot read the response to a fetch request sent to https://yourapi.com. CORS is a controlled relaxation of that rule. When your server sends Access-Control-Allow-Origin: https://yourfrontend.com, it tells the browser that one specific origin is allowed to read the response.
Two things are critical to understand before we go further:
- CORS controls whether a browser script can read a response. It does not prevent the request from being sent. Your server still processes the request; CORS only gatekeeps the client's ability to see the result.
- CORS headers are enforced by the browser. A server-side script, curl, or Postman ignores them entirely. If your API relies on CORS as its only protection, any non-browser client bypasses it completely.
What You'll Learn
- Why wildcard origins combined with credentials are dangerous even when browsers block them by default
- How reflecting the request's
Originheader without validation hands attackers carte blanche - The
nullorigin loophole and why it's exploitable from sandboxed iframes - How loose regex patterns let attackers register look-alike domains
- Concrete header configurations that fix each class of problem
The Wildcard + Credentials Combination
The most well-known CORS rule is that you cannot combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. Browsers will block the response if you try. So teams that need credentialed cross-origin requests (cookies, HTTP auth) switch to reflecting the origin dynamically β and that is where things go wrong.
But even a plain wildcard without credentials creates real risk for APIs that rely on network-level access control. If your API is internal and expects only intranet clients, a wildcard CORS policy lets a malicious external page trigger requests from an internal user's browser and read the responses.
# Dangerous: wildcard on an internal-facing API
Access-Control-Allow-Origin: *
# Safe for truly public, read-only, unauthenticated data only
Access-Control-Allow-Origin: *Wildcard is only safe when the data is genuinely public, requires no authentication, and carries no user-specific content. If any of those conditions are false, use an explicit allowlist.
Reflecting the Request Origin Without Validation
This is the most dangerous pattern in the wild. Some servers are configured to take whatever value appears in the incoming Origin header and echo it back as Access-Control-Allow-Origin. The intent is usually convenience.
Developers want:
- Multiple environments
- Local development support
- Staging deployments
- Partner integrations
Instead of maintaining an allowlist, they simply echo the incoming Origin value.
Example:
origin = request.headers.get("Origin")
response.headers[
"Access-Control-Allow-Origin"
] = origin
At first glance this seems harmless.
The browser sends:
Origin: https://app.company.com
and receives:
Access-Control-Allow-Origin:
https://app.company.com
Everything works.
The problem is that an attacker controls the Origin header.
A malicious site:
https://evil-example.com
produces:
Access-Control-Allow-Origin:
https://evil-example.com
If credentials are also allowed:
Access-Control-Allow-Credentials: true
the attacker's JavaScript can now read authenticated API responses.
Effectively, you've disabled the browser's same-origin protection.
Dangerous Pattern
response.headers[
"Access-Control-Allow-Origin"
] = request.headers["Origin"]
without validation.
Safe Pattern
ALLOWED_ORIGINS = {
"https://app.company.com",
"https://admin.company.com"
}
origin = request.headers.get("Origin")
if origin in ALLOWED_ORIGINS:
response.headers[
"Access-Control-Allow-Origin"
] = origin
Always validate against an explicit allowlist.
The Null Origin Loophole
Many developers are surprised to learn that:
Origin: null
is a legitimate browser value.
It appears in scenarios such as:
- Sandboxed iframes
- Local files
- Certain data URLs
- Browser extensions
- Embedded content
Some CORS implementations accidentally allow it.
Example:
Access-Control-Allow-Origin: null
or:
if origin:
allow(origin)
where:
null
passes validation.
An attacker can create a sandboxed iframe that generates requests with a null origin and gains access to data intended only for trusted sites.
Unless you have a very specific requirement, reject:
Origin: null
entirely.
Loose Regex Allowlists
Many teams try to avoid maintaining a large allowlist by using pattern matching.
Example:
if re.match(
r".*company\.com",
origin
):
allow(origin)
This looks reasonable.
It isn't.
These domains match:
company.com
app.company.com
but so do:
company.com.attacker.net
evilcompany.com
depending on how the regex is written.
Attackers routinely exploit sloppy pattern matching.
Dangerous Regex
.*company\.com
Safer Regex
^https://([a-z0-9-]+\.)?company\.com$
Even better:
Avoid regex when possible.
Explicit allowlists are simpler to audit and harder to bypass.
Subdomain Takeover Risk
Sometimes the allowlist itself is correct.
The problem is ownership.
Example:
https://blog.company.com
appears in the CORS allowlist.
Months later:
- The service is decommissioned
- DNS remains
- The hosting provider releases the resource
An attacker claims the abandoned subdomain.
Your API still trusts it.
Now attacker-controlled JavaScript receives access to authenticated responses.
This is one reason security teams periodically audit:
- DNS records
- Allowed origins
- Decommissioned services
Unused subdomains are surprisingly valuable attack targets.
Allowing Too Many Development Origins
Development environments often create temporary exceptions:
http://localhost:3000
http://localhost:5173
http://127.0.0.1:8080
That's normal.
Problems arise when teams get lazy and deploy:
if "localhost" in origin:
allow(origin)
or:
allow("http://*")
Development shortcuts have a tendency to survive production deployments.
Keep:
- Development allowlists
- Staging allowlists
- Production allowlists
separate.
Misconfigured Preflight Responses
Many APIs correctly protect:
GET
POST
requests but accidentally over-permit preflight checks.
Example:
Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: *
While not automatically exploitable, excessively broad preflight responses increase attack surface and make future mistakes more dangerous.
A safer approach:
Access-Control-Allow-Methods:
GET, POST
Access-Control-Allow-Headers:
Content-Type, Authorization
Only allow what the application genuinely requires.
Exposing Sensitive Response Headers
Developers often focus only on:
Access-Control-Allow-Origin
and forget:
Access-Control-Expose-Headers
Example:
Access-Control-Expose-Headers: *
or:
Access-Control-Expose-Headers:
Authorization,
X-API-Key,
Session-ID
This can unintentionally expose sensitive metadata to browser JavaScript.
Only expose headers necessary for application functionality.
CORS Does Not Replace Authentication
One of the most dangerous misconceptions is:
Only our frontend can call the API.
because:
CORS allows only our frontend.
This is false.
Remember:
- Browsers enforce CORS
- Attackers can use curl
- Attackers can use Postman
- Attackers can write server-side code
Example:
curl https://api.company.com/users
ignores CORS completely.
Authentication and authorization must stand on their own.
CORS should be treated as a browser security layer, not an access-control mechanism.
How to Audit Your Existing CORS Policy
Review every API endpoint and answer:
Question 1
What origins are allowed?
Explicit list?
Regex?
Wildcard?
Reflection?
Question 2
Are credentials enabled?
Access-Control-Allow-Credentials: true
If yes, validation must be extremely strict.
Question 3
Is null allowed?
Reject it unless absolutely necessary.
Question 4
Can unused domains still access the API?
Review subdomains and historical entries.
Question 5
Do development origins exist in production?
They often linger longer than expected.
Example: Safe Production Configuration
A typical secure setup:
Access-Control-Allow-Origin:
https://app.company.com
Access-Control-Allow-Credentials:
true
Access-Control-Allow-Methods:
GET, POST, PUT
Access-Control-Allow-Headers:
Authorization,
Content-Type
Server-side validation:
ALLOWED_ORIGINS = {
"https://app.company.com"
}
origin = request.headers.get("Origin")
if origin in ALLOWED_ORIGINS:
response.headers[
"Access-Control-Allow-Origin"
] = origin
No reflection.
No wildcard.
No regex.
No surprises.
Testing Your Configuration
Simple manual testing catches many issues.
Try:
curl -H "Origin: https://evil.com"
https://api.company.com
Check:
Access-Control-Allow-Origin
If:
https://evil.com
appears in the response, investigate immediately.
Repeat for:
null
and suspicious look-alike domains.
Automated security scans can help, but a few targeted tests often reveal the biggest problems.
Common CORS Mistakes
Reflecting Origins Automatically
The most dangerous pattern.
Trusting Regex Too Much
Regex is harder to secure than explicit lists.
Allowing Null Origins
Rarely necessary.
Often risky.
Leaving Development Rules in Production
A surprisingly common source of exposure.
Treating CORS as Authentication
CORS is not access control.
Never rely on it as such.
Best Practices Checklist
β Use explicit allowlists
β Validate every Origin value
β Reject null origins
β Limit allowed methods
β Limit allowed headers
β Review subdomains periodically
β Separate development and production configurations
β Test with malicious origins
β Treat authentication independently of CORS
β Review policies during every security audit
Final Thoughts
CORS vulnerabilities rarely announce themselves. They don't crash applications, generate stack traces, or trigger monitoring alerts. Instead, they quietly transform a browser security control into a data-exposure mechanism that attackers can exploit from domains they control.
The most common causes are surprisingly simple: reflecting Origin headers without validation, trusting loose regex patterns, allowing null origins, or leaving development shortcuts in production. Each of these decisions may seem harmless in isolation, but together they can grant unauthorized websites access to authenticated API responses.
The safest approach is also the simplest. Use explicit allowlists, validate origins carefully, keep production policies narrow, and remember that CORS complements authentication rather than replacing it. If your API remains secure even when CORS is removed entirely, you're probably relying on the right controls.
Understanding these patterns turns CORS from a confusing checkbox into a deliberate security mechanismβone that protects users instead of silently exposing them.
Frequently Asked Questions
Can a CORS misconfiguration be exploited without the user doing anything suspicious?
Yes. The attack requires only that a victim visits the attacker's page while logged into your service. The attacker's JavaScript silently fetches your API using the victim's credentials, and a permissive CORS policy allows the attacker's script to read the response.
Does setting Access-Control-Allow-Origin to a wildcard protect against credentialed requests?
Browsers block credentialed requests (those with cookies or HTTP auth) when the server returns a wildcard origin. However, attackers can still exploit a wildcard to read non-credentialed responses, which matters for internal APIs or any API that relies on network-level access controls rather than per-request authentication.
How do I test whether my API has a CORS misconfiguration?
Send an OPTIONS preflight request using curl with a crafted Origin header set to an unrecognized domain and inspect the Access-Control-Allow-Origin value in the response. If the server echoes back your fake origin or returns a wildcard, the policy is misconfigured.
Is it safe to allow CORS from all subdomains of my own domain?
No. Trusting all subdomains means a compromised or abandoned subdomain instantly becomes a trusted CORS origin. Use an explicit allowlist of specific subdomains rather than a wildcard or suffix match.
Does a correct CORS policy protect my API from CSRF attacks?
Not completely. CORS controls whether a browser script can read a cross-origin response, but simple requests such as form posts can still be triggered cross-origin without a preflight. You need a separate CSRF mitigation strategy, such as requiring custom headers or using the SameSite cookie attribute.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!