CORS ACAM credential bypass patterns
TL;DR: Classic CORS misconfig (cors-misconfig) covers
Access-Control-Allow-Origin: *+Access-Control-Allow-Credentials: true(browser blocks that combo, but old SDKs / non-browser clients don’t). This note covers the more subtle patterns: regex-based origin reflection bypass, null-origin abuse, wildcard subdomain trust where one subdomain is attacker-controllable,Vary: Origincache pollution, and pre-flight cache poisoning. The ACAM (“Access-Control-Allow-Method/Headers”) side is often under-checked.
What it is
CORS is enforced by the browser on JS-initiated cross-origin requests. The server’s Access-Control-Allow-* headers tell the browser whether to deliver the response to JS. Misconfig categories:
ACAO(Origin) too permissive — most reports focus here.ACAC(Credentials) + reflected origin — credentialed cross-origin read.ACAM(Methods) +ACAH(Headers) too permissive — allows arbitrary preflighted requests.- Pre-flight cache poisoning — stale
ACAHlets attacker re-use headers.
Patterns
1. Origin reflection with weak regex
Server reads Origin: https://victim.com.attacker.com and reflects with ACAO. If regex is ^https://(.*\.)?victim\.com$ it correctly rejects, but ^https://.*victim\.com.*$ or substring match allows.
- Audit: every reflection point, see if there’s an allowlist with strict anchors.
- Common shortcuts that fail:
endsWith('victim.com')matchesattackervictim.com.
2. null origin acceptance
Some sandboxed contexts send Origin: null:
- Sandboxed iframe (
<iframe sandbox>withoutallow-same-origin). - Local file (
file://). - Redirected request through certain protocols.
- Server allowing
null→ attacker creates an iframe sandbox at any origin, makes credentialed request. - Fix: never allowlist
null.
3. Wildcard subdomain + takeable subdomain
- Allowlist:
^https://.*\.victim\.com$. legacy.victim.compoints to a deprovisioned Heroku → attacker claims it.- Now attacker hosts JS on
https://legacy.victim.comthat makes credentialed cross-origin requests toapp.victim.com. - Combine: subdomain-takeover, dangling-dns-takeover.
4. Allowed scheme confusion
- Allowlist:
victim.comwithout scheme check. - Attacker hosts
http://victim.com(no TLS) — under proxy or mitm context. - Or
wss://victim.comfor WebSocket-origin trust.
5. Vary: Origin cache pollution
- Server sends
ACAO: https://app.victim.comfor one user. - If
Vary: Originmissing, CDN caches the response. - Next user (different origin) gets the cached headers — ACAO mismatches, browser rejects, but the actual response body is delivered to the cache, which means anyone with the cache key can read it.
- Fix: always
Vary: Originon any reflected response.
6. Preflight ACAH too permissive
- Server responds to OPTIONS with
Access-Control-Allow-Headers: *or includes every request header. - Attacker can attach arbitrary headers (
Authorization,Cookie-equivalent via headers, custom tokens) that bypass server-side filtering. - Fix: list only allowed headers.
7. Preflight cache poisoning
- Browser caches preflight result per
Access-Control-Max-Age. Default Chrome is 2hr. - If server has a brief misconfig (deploys an allowlist change badly), browsers retain the permissive preflight for hours.
- Attacker times the request during the misconfig window; preflight cached; even after fix, attacker still has access for the cache duration.
- Fix: short
Max-Age(under 600s); careful staged deploys.
8. ACAC: true reflected by mistake
Server reflects origin and credentials. Browser will accept credentialed cross-origin response. Auth cookie sent. Attacker reads response.
- Mistake pattern:
app.use((req,res,next)=>{ res.header('ACAO', req.headers.origin); res.header('ACAC', 'true'); next() })— naive Express middleware.
9. Non-browser HTTP clients ignore CORS
- Postman, curl, mobile apps, server-to-server — none enforce CORS.
- If your server returns
ACAO: *+ sensitive data assuming browser blocks, but a separate path (mobile API) returns the same data with auth → not a CORS bug, but an authn bug. - CORS is a browser feature; never rely on it as authentication.
10. PostMessage as CORS bypass
- iframe with
target.postMessage(data, '*')sends data to any origin. - Not technically a CORS bypass, but achieves same goal: cross-origin data flow.
- See postmessage-bugs.
Testing methodology
Black-box
- Find every API endpoint that returns sensitive data or accepts credentials.
- For each: send with
Origin: https://attacker.com, observeACAOecho. - Test
Origin: null,Origin: https://victim.com.attacker.com,Origin: https://attackervictim.com. - Look for
Vary: Originheader. - Send OPTIONS preflight with
Access-Control-Request-Headers: X-Custom; see whatACAHreturns. - Test allowed subdomain expansion: known takeover candidates.
Source review
- Find all CORS middleware:
corsnpm package,flask-cors,django-cors-headers,aspnet-cors, manualAccess-Control-*setting. - For each: read the origin allowlist logic. Look for regex, substring match, fallback to wildcard.
- Check
ACACsettings. - Check that
Vary: Originis set.
Hardening checklist
- Strict origin allowlist with exact match, no regex.
Access-Control-Allow-Credentials: trueonly on endpoints that truly need it.Vary: Originon any response with reflected origin.- Audit every
*.yourdomain.comfor takeover risk (you wildcard-trust them). - Preflight
Max-Age≤ 600s during config change windows; longer otherwise but rotate audits. - No CORS = no cross-origin JS read; reserve CORS for the specific endpoints that need it.