Scanverra
Back to Articles
Security

Understanding CORS: A Practical Guide for Web Developers

·8 min read

CORS errors are one of the most common sources of "why won't this just work" frustration in web development - and also one of the most common places a fix gets copy-pasted without anyone understanding what it actually opened up.

The Same-Origin Policy, First

CORS only exists because of a more fundamental browser rule: the same-origin policy. By default, a script running on https://app.example.com cannot read the response of a request made to https://api.other.com - even if the request itself succeeds. Two URLs share an origin only if the protocol, hostname, and port all match exactly.

This isn't about preventing the request from happening - it's about preventing the calling page's JavaScript from reading the response. The request still reaches the server; the browser just won't hand the result back to the script that asked for it, unless the server explicitly says it's fine to.

What CORS Headers Actually Do

CORS is the server opting back into cross-origin access, one header at a time:

  • Access-Control-Allow-Origin - which origin(s) are allowed to read the response.
  • Access-Control-Allow-Credentials - whether cookies/HTTP auth are included and readable cross-origin.
  • Access-Control-Allow-Methods - which HTTP methods are permitted cross-origin.
  • Access-Control-Allow-Headers - which custom request headers the browser is allowed to send.

Simple Requests vs. Preflight

Not every cross-origin request works the same way. A "simple" request (GET/POST with only a small set of allowed headers and content types) goes straight through, and the browser checks Access-Control-Allow-Origin on the actual response. Anything more complex - a custom header, a method like PUT or DELETE, a JSON content type - triggers a preflight: the browser first sends an OPTIONS request asking permission, and only sends the real request if the server approves.

The Mistake That Turns a Convenience Header Into a Real Exposure

Access-Control-Allow-Origin: *is the fastest way to make a CORS error disappear during development. On its own, for a genuinely public, non-sensitive endpoint, it's not inherently dangerous. It becomes critical the instant it's paired with:

Access-Control-Allow-Credentials: true

Browsers are supposed to reject this specific combination outright, but plenty of servers still send both - and a server sending it at all is a sign the policy was switched on to silence an error, not actually designed. If the endpoint returns anything tied to a logged-in session, this pairing means any site on the internet can read it.

A Safer Pattern

  • Maintain an explicit allowlist of trusted origins rather than a wildcard.
  • Only set Allow-Credentials: true alongside a specific validated origin, never a wildcard.
  • Scope CORS per-route - a public read-only endpoint and an authenticated API rarely need identical policies.
  • Remember preflight (OPTIONS) requests need the same allowlist logic as the real request.

How to Test For It

curl and most non-browser HTTP clients ignore CORS entirely, which is exactly why a broken or overly-permissive policy often goes unnoticed until real browser JavaScript hits it. A security scan that inspects your actual response headers - the way a browser would enforce them - is the more reliable way to catch a misconfigured CORS policy before it becomes a live incident.

Find out which headers you're missing

Run a free security scan and get a plain-English breakdown of every header, cert, and exposed secret.

Run a free security scan