Vulnerabilities
Access-Control-Expose-Headers vs CORS Allow-Origin
January 10, 2022
Access-Control-Expose-Headers vs CORS Allow-Origin is two Fetch CORS knobs. Access-Control-Allow-Origin decides which browser origins may read the response at all. Access-Control-Expose-Headers names extra response headers that script may read besides the CORS-safelisted ones (Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma). Exposing Authorization, Set-Cookie, or a debug request-id to * callers is how you donate metadata. Timing-Allow-Origin is yet another header for Resource Timing, not ACEH. RoastMyOpsec app mode sends a bounded untrusted Origin and reads ACAO and credentials. It does not score Access-Control-Expose-Headers, does not read custom headers as a user, and does not attempt to skip CORS.
The practical difference
ACAO is 'who.' ACEH is 'which headers that who may inspect.' A public weather API can use ACAO * and a short ACEH list. A cookie API cannot use ACAO * at all — see credentials vs wildcard. Max-Age only caches preflight; it does not expose headers.
Safelisted headers are already readable after a successful CORS check. You do not need to expose Content-Type. You do need ACEH for X-Request-Id if the SPA reads it — and you should ask whether that ID belongs in JS.
How to choose what to expose
Expose only headers the first-party SPA must read. Prefer a named ACAO. Do not expose Set-Cookie (browsers will not give JS cookies anyway if HttpOnly). Do not expose WWW-Authenticate details. Do not copy ACEH * from a gateway default onto an authenticated API.
| Header | Job | JS can read after CORS? | Takeaway |
|---|---|---|---|
| Access-Control-Allow-Origin | Which origin may read the response | N/A — it is the gate | Allowlist; see wildcard guide |
| Access-Control-Expose-Headers | Which extra headers JS may read | Only names you list (plus safelist) | Minimize; no debug dumps |
| Timing-Allow-Origin | Resource Timing visibility | Different API | See TAO vs CORS |
| Access-Control-Max-Age | Preflight cache TTL | No | See Max-Age vs preflight |
What the roast can prove
ACAO reflecting an untrusted Origin can surface. A generous ACEH list is not scored today. Hunt Expose-Headers in your gateway. Pair with verbose API errors if the exposed header is a stack trace id.
Common mistakes
The first mistake is Access-Control-Expose-Headers: * on a user API.
The second mistake is exposing a header that contains a signed URL.
The third mistake is tightening ACAO and leaving ACEH copied from a public CDN preset.
Free audit the URL you own
RoastMyOpsec is a defensive public-surface roast: headers, cookies, sensitive paths, and more — no exploit payloads. Start with the free audit, then open the vault if the blurred findings look expensive.
Free audit nowFAQ
- Does Expose-Headers let other sites call my API?
- No. Allow-Origin (and credentials) decide who may read. ACEH only names extra headers after that gate passes.
- Should I expose X-Request-Id?
- Only if the SPA must show it. Prefer logging it server-side. Do not expose it to every origin.
- Does RoastMyOpsec score Access-Control-Expose-Headers?
- No. It reads ACAO and credentials from a bounded Origin.