All playbooks

Playbook 03 / 17

Debugging CORS Origin Bugs

A practical way to inspect origin parsing, credentials, and header behavior.

The pattern

An allow-list check parses the Origin header too loosely - substring matches, prefix matches, or regexes that accept lookalike hosts - so the wrong origin is accepted or a legitimate one rejected. CORS bugs are invisible in curl and server-to-server tests because only browsers enforce the policy. The bug lives in string handling, not in the browser.

( 01 )Symptoms

How this failure announces itself.

  • warningRequests succeed from curl, Postman, and tests, but fail only in the browser.
  • warningCredentialed requests (cookies, Authorization) are rejected while anonymous ones pass.
  • warningAn origin you never intended to allow turns out to work in production - often found by a security review.
  • warningPreflight (OPTIONS) responses disagree with the headers on the actual response.
( 02 )First moves

The first ten minutes — establish facts before touching code.

  • 1Reproduce in the browser console with fetch and read the exact CORS error - it names the failing header.
  • 2Inspect the preflight exchange in the network tab: what Origin was sent and what Allow-Origin came back?
  • 3Find the server code that produced that Allow-Origin value and trace which branch of the allow-list matched, or failed to.
  • 4Probe the matcher with adversarial origins (evil-example.com, example.com.evil.net, http vs https) to map its real behavior.
( 03 )Where to look

The code and config that usually owns this bug.

  • searchThe origin parsing code - is it comparing full URLs, hostnames, or raw strings?
  • searchAllow-list checks built on startsWith, includes, endsWith, or hand-written regexes - each has a known bypass shape.
  • searchCredential handling - Access-Control-Allow-Credentials combined with a wildcard or blindly reflected origin is a spec violation browsers enforce.
  • searchSubdomain and suffix matching - evil-example.com passes an endsWith('example.com') check.
( 04 )Common fixes

Fix the cause, then make the regression impossible.

  • buildParse origins as URLs and compare scheme and hostname (and port where it matters) - never raw substring checks.
  • buildCompare hostnames exactly, or against a suffix rule anchored on a dot boundary (.example.com).
  • buildNever reflect arbitrary origins when credentials are allowed; echo only exact allow-list matches.
  • buildAdd edge-case tests for lookalike domains, scheme mismatches, and credentialed preflights.
( 05 )Prove the fix

A fix you can't demonstrate is a guess. Close the loop.

  • verifiedThe original failing browser flow now passes with the expected Allow-Origin echoed.
  • verifiedAdversarial origins are rejected in tests - including the lookalike that previously slipped through.
  • verifiedCredentialed and anonymous paths both behave per spec, with no wildcard-plus-credentials combination.