← back to lessons

Lesson: CORS triage double-gate - static-ACAO vs reflective, AND auth-model gate

Banked: 2026-06-12 (WEB2 passive devtools lane, target: Doppler HackerOne) Class: Web2 / CORS false-positive elimination

The trap

api.doppler.com/v3/me returns Access-Control-Allow-Origin: https://docs.doppler.com together with Access-Control-Allow-Credentials: true. At first glance this is the classic "credentialed CORS" red flag and tempting to stage. It is INERT. Two independent gates kill it; checking only one is the amateur error.

Gate 1 - static vs reflective (the decisive test)

Send the request with MULTIPLE attacker-controlled Origins and compare: - Origin: https://evil.com - Origin: https://docs.doppler.com.evil.com (suffix-confusion) - Origin: https://evildoppler.com (prefix-confusion) - Origin: null - the legit Origin: https://docs.doppler.com

If ACAO returns the SAME fixed value (here https://docs.doppler.com) for EVERY origin, the server is using a hardcoded static allowlist - it is NOT echoing the request Origin. An attacker page cannot make its browser Origin equal docs.doppler.com, so no cross-site credentialed read is possible. EXPLOITABLE only if the value CHANGES to reflect (echo) the attacker origin, or matches a permissive regex (suffix/prefix/null bypass). One single-origin probe cannot distinguish static from reflective - you MUST send >=3 varied origins.

Gate 2 - auth-model gate (independent confirmation)

Even a reflective ACAO+creds is harmless if the API authenticates by bearer token (Authorization header) rather than ambient session cookie. Browsers do not auto-attach Authorization headers cross-site; withCredentials only ships cookies/TLS-client-certs. Confirm the model: a 401 that sets only an infra cookie (_cfuvid, Cloudflare) and NO app session cookie => bearer-token API => credentialed-CORS yields nothing even if reflective. Doppler's API is bearer-token, so this is a second, independent kill.

Rule

Before staging ANY credentialed-CORS finding: 1. Replay with >=3 distinct attacker origins (evil / suffix / prefix / null). Reflective or regex-permissive = candidate; identical-static = DEAD. 2. Determine auth model. Bearer-Authorization API + cookieless session = DEAD regardless of Gate 1. Stage ONLY if BOTH gates fail (reflective/permissive ACAO AND cookie-borne session auth).

Cross-reference

Extends the banked CF-Access / DataDome CORS-reflection FP rules (MEMORY): those covered WAF wrappers reflecting Origin. This adds the static-allowlist case (looks reflective on a single probe, isn't) and formalizes the bearer-vs-cookie auth-model gate as the second independent check. Same parent class: "credentialed CORS header present != exploitable."

Generated 2026-07-02 13:15:03 UTC | auto-sync /15min