← Back to Blog

CORS misconfigurations are the data leak nobody checks for

CORS misconfigurations are the data leak nobody checks for

Cross-Origin Resource Sharing is one of those things that works fine until it doesn't, and when it doesn't, the failure mode is either "nothing loads and nobody knows why" or "any website can read your users' private data." There is very little middle ground.

The browser's same-origin policy exists for a reason: it prevents a random website from making authenticated requests to your banking app and reading the response. CORS is the controlled exception to that rule. It lets your API say "yes, requests from app.example.com are allowed to read my responses." The problem is that most CORS configurations are set up by a developer who just wanted the error to go away, and the quickest way to make a CORS error go away is Access-Control-Allow-Origin: *.

The wildcard credentials trap

Access-Control-Allow-Origin: * is fine for truly public resources. A public CDN serving jQuery doesn't care who loads it. But the moment your API also sends Access-Control-Allow-Credentials: true, you have a real security problem.

Browsers actually block this combination — the spec says you can't use a wildcard origin with credentials. But developers hit this wall, Google the error, and "fix" it by reflecting the requesting origin back in the Access-Control-Allow-Origin header without validating it. Now any website can send credentialed requests to your API and read the response, because your server just echoes back whatever origin the browser sends.

This is origin reflection, and it's the single most common CORS vulnerability. It's functionally equivalent to having no CORS policy at all, except it looks like you have one.

What actually goes wrong

A CORS misconfiguration becomes an exploit like this: an attacker puts JavaScript on their website that makes a fetch() request to your API with credentials: 'include'. The browser sends the request with the user's cookies. Your server sees the Origin: https://evil.example header, reflects it back as Access-Control-Allow-Origin: https://evil.example, and adds Access-Control-Allow-Credentials: true. The browser sees these headers, decides the response is safe to share, and the attacker's JavaScript reads the user's account data, saved payment methods, private messages, whatever the API returns.

The user visited a malicious webpage. That's it. No phishing, no malware install, no social engineering beyond "click this link." The browser did exactly what the CORS headers told it to do.

The six checks that matter

Wildcard + credentials. Does the server send Access-Control-Allow-Origin: * alongside Access-Control-Allow-Credentials: true? Browsers block it, but the server config is still wrong and tells you the developer doesn't understand the policy.

Origin reflection. Does the server echo back any origin it receives? Send a request with a fake origin and see if it comes back in the response header.

Vary: Origin. If the server returns different CORS headers for different origins (which it should, if it validates origins), it must include Vary: Origin in the response. Without it, CDN caches can serve the wrong CORS headers to the wrong origin.

Access-Control-Allow-Methods. Does the preflight response restrict which HTTP methods are allowed? A blanket * on methods means any site can send PUT, DELETE, PATCH requests to your API.

Access-Control-Expose-Headers. Are you accidentally exposing internal headers like X-Request-Id, X-Internal-Trace, or custom auth headers to cross-origin JavaScript?

Access-Control-Max-Age. Without this header, browsers send a preflight OPTIONS request before every single cross-origin request. Setting Max-Age: 86400 caches the preflight result for 24 hours, cutting the number of preflight requests significantly.

What the tool checks

The CORS Headers Audit sends requests to your URL with various origin headers and examines the response. It checks all six conditions above, flags the dangerous combinations, and reports whether your CORS policy is actually protecting anything or just giving the appearance of a policy.

It also checks Timing-Allow-Origin, which controls whether cross-origin JavaScript can read performance timing data (Resource Timing API). This is a lower-severity issue but can leak information about server response times and resource sizes to third-party scripts.

Fixing CORS properly

The fix is straightforward but requires discipline: maintain an explicit allow-list of origins. Don't reflect the origin header blindly. Check the incoming origin against your list, and only set the Access-Control-Allow-Origin header if the origin is on the list. If it's not, don't set the header at all.

In Express.js, this means using the cors middleware with an origin function that validates against your list. In Nginx, it means an if ($http_origin ~* "^https://(app|admin)\.example\.com$") block. In Apache, it means a SetEnvIf Origin directive.

Always add Vary: Origin when you return origin-specific CORS headers. Always limit Access-Control-Allow-Methods to the methods your API actually uses. Keep Access-Control-Expose-Headers to the minimum set the client needs.

If you're setting up API security from scratch and want to understand how headers, CORS, and CSP fit together in a deployment checklist, The $97 Launch on Kindle covers the security header stack for static and API-backed sites.

Fact-check notes and sources

Related reading

This post is informational, not security-consulting advice. Mentions of PortSwigger, MDN, and other third parties are nominative fair use. No affiliation is implied.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026