CORS Security Audit Checklist

You have just inherited an API. It works, the frontend loads, and nobody remembers who set up CORS. Before you trust it, you need to verify — item by item — that its cross-origin configuration is not quietly handing authenticated data to any website that asks. This page turns the full audit procedure into a scannable checklist you can copy into a ticket and work through, each item paired with the exact curl probe that proves it. It is the practical companion to CORS Security Auditing & Hardening, which explains the threat model and reasoning behind each check.

The symptom this page addresses

There is no browser error to chase here. A misconfigured CORS policy is invisible from your own trusted origin — the site works perfectly. The danger only appears when an attacker’s origin makes the request, and the response headers that would reveal the flaw are ones you never see in normal use. The checklist forces you to send those attacker-shaped requests deliberately.

Root cause of most findings

Nearly every item below traces to one root behaviour: the server decides whether to lift the browser’s same-origin read barrier based on the incoming Origin header, and it makes that decision too permissively. The WHATWG Fetch Standard requires that a credentialed cross-origin response carry an Access-Control-Allow-Origin that is an exact match of the request Origin — never a wildcard — so any logic that reflects an unvalidated, null, or lookalike origin, especially with credentials, breaks the boundary the browser is trying to enforce.

Prerequisite state

Before you start, gather:


The Checklist

Work top to bottom. The first group is highest severity; do not skip ahead. Substitute your real host and paths into each probe.

Origin validation

# One pass over the origin-validation probes. A safe server reflects NONE of these.
for o in "https://evil.example" "null" "https://evil-example.com" \
         "https://example.com.attacker.io" "http://app.example.com"; do
  printf '%-34s -> ' "$o"
  curl -sS -m 10 -D - -o /dev/null https://api.example.com/v1/account -H "Origin: $o" \
    | grep -i '^access-control-allow-origin:' | tr -d '\r' || echo '(none)'
done

Credentials

# Flag the critical combo: attacker origin reflected AND credentials enabled.
curl -sS -m 10 -D - -o /dev/null https://api.example.com/v1/account \
  -H "Origin: https://evil.example" \
  | grep -iE '^access-control-allow-(origin|credentials):' | tr -d '\r'

Headers (preflight surface)

# Inspect the preflight response for an untrusted origin.
curl -sS -m 10 -X OPTIONS -D - -o /dev/null https://api.example.com/v1/account \
  -H "Origin: https://evil.example" \
  -H "Access-Control-Request-Method: DELETE" \
  -H "Access-Control-Request-Headers: Authorization, X-Custom" \
  | grep -iE '^(access-control|vary):' | tr -d '\r'

Caching / Vary

# Reflected origin must be accompanied by Vary: Origin.
curl -sS -m 10 -D - -o /dev/null https://api.example.com/v1/account \
  -H "Origin: https://app.example.com" \
  | grep -iE '^(access-control-allow-origin|vary):' | tr -d '\r'

Proxy / edge layer

# Count Access-Control-Allow-Origin occurrences — must be 0 or 1, never 2.
curl -sS -m 10 -D - -o /dev/null https://api.example.com/v1/account \
  -H "Origin: https://app.example.com" \
  | grep -ci '^access-control-allow-origin:'

Verification

After remediating any finding, re-run the two probes that matter most and confirm the expected output:

# 1) Untrusted origin: expect NO access-control-allow-origin line at all.
curl -si https://api.example.com/v1/account -H "Origin: https://evil.example" \
  | grep -i access-control-allow-origin

# 2) Trusted origin: expect an exact reflect AND vary: origin.
curl -si https://api.example.com/v1/account -H "Origin: https://app.example.com" \
  | grep -iE '^(access-control-allow-origin|vary):'

The first command should print nothing; the second should print exactly the allowed origin plus vary: origin. In DevTools, open Network, trigger the real cross-origin request from the trusted app, and confirm the response shows a single access-control-allow-origin matching the page origin.

Security note

Do not “fix” a finding by widening to Access-Control-Allow-Origin: *. A wildcard removes the browser’s ability to attach credentials, which will silently break authenticated flows, and it is the wrong tool for any endpoint that serves per-user data. The correct remediation is an exact allowlist with reflection only on a match — the decision between the two is covered in Wildcard vs Dynamic Origin Reflection: When to Use Each. Never add the string null to an allowlist to make a sandboxed-iframe integration work.

Common Mistakes

Mistake What it hides Fix
Testing only from the trusted app The reflection flaw never appears from an allowed origin Always probe with an untrusted Origin via curl
Checking a sample of routes Per-route overrides on unprobed paths stay hidden Enumerate and probe every endpoint
Probing without a session cookie only CORS headers injected only for authenticated sessions are missed Re-run key probes with and without a valid cookie
Treating * as the fix for a reflection finding Breaks credentialed flows and misses the real remediation Replace with an exact allowlist and reflect on match

FAQ

Which checklist item should I verify first on an inherited API?

Start with the reflected-origin-plus-credentials check, because it is the only combination that exposes authenticated per-user data to any website. Send a request with an Origin header you control and inspect whether Access-Control-Allow-Origin echoes it while Access-Control-Allow-Credentials: true is also present. If both appear, treat it as a live incident and remediate before continuing the rest of the checklist. Everything else on the list is lower severity.

Can I run this checklist against production safely?

Yes. Every probe on this checklist is a read-only GET or OPTIONS request that only sets request headers such as Origin and Access-Control-Request-Method. None of them change server state, and because they are not sent from a browser they carry no credentials. The one caution is rate: run them sequentially rather than flooding the endpoint, and avoid the checklist on endpoints that count OPTIONS requests toward a billing or quota metric.

Does a clean checklist mean the API has no CORS risk at all?

It means the externally observable configuration is sound for the origins and paths you tested. It does not cover origins added later, per-route overrides you did not probe, or CORS headers injected only under specific conditions such as an authenticated session or a particular Accept header. Re-run the checklist in CI after every deploy, enumerate every route rather than a representative sample, and repeat the probes both with and without a valid session cookie.