EncryptCodecencryptcodec
Blog/Web Security
Web SecurityMarch 29, 2026 · 7 min read

CORS Explained: The 5 Most Dangerous Misconfigurations and How to Fix Them

CORS is not a security mechanism — it is a controlled relaxation of the Same-Origin Policy. Every misconfiguration makes your API more permissive, not less.

What CORS Actually Does

The Same-Origin Policy prevents evil.com from making JavaScript requests to yourapi.com and reading the response. CORS (Cross-Origin Resource Sharing) creates exceptions to this rule.

When a browser makes a cross-origin request, the server responds with headers that say "I allow requests from this origin." If the headers are missing or wrong, the browser blocks the response.

GET /api/user HTTP/1.1
Host: api.yourapp.com
Origin: https://yourapp.com

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://yourapp.com

The critical thing to understand: CORS is enforced by the browser, not the server. The server always processes the request and sends the response. The browser just decides whether JavaScript can read that response.

The 5 Dangerous Misconfigurations

1. Wildcard with Credentials

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

This is the most common mistake. Browsers actually block this combination — Access-Control-Allow-Origin: * cannot be used with credentials: true. But developers "fix" this by reflecting the origin instead (see #2), which is worse.

What to do: Never use * if your API uses cookies, sessions, or any form of credentials. Whitelist specific origins.

2. Reflecting the Origin Header

// ❌ Extremely dangerous
app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  res.setHeader("Access-Control-Allow-Credentials", "true");
  next();
});

This mirrors whatever origin the browser sends — including https://evil.com. An attacker's site can now make authenticated requests to your API and read the responses, stealing user data.

What to do: Validate the origin against a whitelist:

const ALLOWED_ORIGINS = [
  "https://yourapp.com",
  "https://staging.yourapp.com",
];
 
app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (ALLOWED_ORIGINS.includes(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader("Vary", "Origin");
  }
  next();
});

3. Trusting null Origin

// ❌ Dangerous — null origin can be forged
if (origin === "null" || ALLOWED_ORIGINS.includes(origin)) {
  res.setHeader("Access-Control-Allow-Origin", origin);
}

The null origin comes from sandboxed iframes, local files, and redirects. An attacker can craft a sandboxed iframe that sends requests with Origin: null:

<iframe sandbox="allow-scripts" src="data:text/html,
  <script>
    fetch('https://api.yourapp.com/user', {credentials: 'include'})
      .then(r => r.json())
      .then(d => parent.postMessage(d, '*'));
  </script>
"></iframe>

What to do: Never whitelist null as an allowed origin.

4. Missing Vary: Origin Header

When your CORS response depends on the Origin header, you must include Vary: Origin. Without it, CDNs and browser caches may serve the wrong CORS headers:

# Request from app-a.com gets cached
Access-Control-Allow-Origin: https://app-a.com

# Later request from app-b.com gets the cached response
# Browser sees app-a.com in the header, blocks app-b.com

What to do: Always include Vary: Origin when the Access-Control-Allow-Origin value changes per request:

res.setHeader("Vary", "Origin");

5. Overly Permissive Preflight

Preflight requests (OPTIONS) check whether the actual request is allowed. Some developers cache preflight responses for too long or allow too many methods and headers:

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-Control-Allow-Headers: *
Access-Control-Max-Age: 86400

What to do: Only allow the methods and headers your API actually uses:

res.setHeader("Access-Control-Allow-Methods", "GET, POST");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Max-Age", "600"); // 10 minutes, not 24 hours

Proper CORS Configuration

Here is a secure CORS setup for Express:

import cors from "cors";
 
const corsOptions = {
  origin: (origin, callback) => {
    const whitelist = [
      "https://yourapp.com",
      "https://staging.yourapp.com",
    ];
    if (!origin || whitelist.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error("Not allowed by CORS"));
    }
  },
  credentials: true,
  methods: ["GET", "POST"],
  allowedHeaders: ["Content-Type", "Authorization"],
  maxAge: 600,
};
 
app.use(cors(corsOptions));

When to Use Wildcard

Access-Control-Allow-Origin: * is fine when:

  • Your API is completely public (no authentication)
  • You serve static assets (fonts, public images)
  • You are a public CDN

It is not fine when your API uses cookies, tokens, or any form of session.

Testing Your CORS Configuration

# Test a cross-origin request
curl -H "Origin: https://evil.com" \
  -H "Access-Control-Request-Method: GET" \
  -I https://api.yourapp.com/user
 
# Check if the response reflects the evil origin
# If Access-Control-Allow-Origin: https://evil.com appears, you are vulnerable

Conclusion

CORS misconfigurations are consistently in the OWASP Top 10 for API security issues. The rules are simple: never reflect arbitrary origins, never trust null, always set Vary: Origin, and whitelist only the domains that need access. Test with curl from an untrusted origin before you ship.

Share this post

Try the CORS Exploit Simulation

Related posts