skip to content

Under credentialed mode, what does `Access-Control-Expose-Headers: *` make readable to the calling script?

level: seniorimportance: nice to knowfreq 30%

answer

  1. the asterisk stops being special
  2. matched as a literal name
  3. only for uncredentialed requests does it expand
  4. seven safelisted response names remain
  5. no error, just a missing value

basics

~20 s

Only a response header whose name is literally *. Once the request's credentials mode is "include", the wildcard in the grant fields stops being a wildcard and is matched as an ordinary field name, so nothing extra is exposed.

solid answer

~30 s

The `*` in `Access-Control-Expose-Headers`, `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` is expanded into "everything" **only** for requests that are not credentialed. When the credentials mode is `"include"`, no expansion happens and the token is compared as a literal name — so `Access-Control-Expose-Headers: *` exposes a header called `*`, which no server sends. Script is left with the CORS-safelisted response-header names it could already read: `Cache-Control`, `Content-Language`, `Content-Length`, `Content-Type`, `Expires`, `Last-Modified` and `Pragma`. The same literal reading applies in a preflight answer, so a credentialed request using a method or header covered only by a `*` fails. Under credentials every name must be spelled out.

code

http · 7 lines
http
HTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 47
Access-Control-Allow-Origin: https://portal.example
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Vary: Origin

go deeper

for a junior

Know that script cannot read arbitrary response headers by default, and that the server names the extra ones it is willing to expose.

for a middle

Explain that the wildcard in the exposure and preflight grants is expanded only for uncredentialed requests, and name the seven headers readable without any grant.

for a senior

Recognise the silent symptom — one missing value, no error anywhere — and know it appears the moment a working feature starts carrying the session.

for a principal

The point to generalise is that the protocol forces credentialed grants to be explicit, which is what makes a header block reviewable rather than inherited.

## The rule Three CORS grant fields accept `*` as a wildcard: - `Access-Control-Expose-Headers` — which response headers script may read; - `Access-Control-Allow-Methods` — which methods a preflight clears; - `Access-Control-Allow-Headers` — which request header names a preflight clears. In each case the expansion of `*` into "all of them" is conditional: it happens only when the request's credentials mode is **not** `"include"`. Under credentials the token is left alone and matched as what it literally is — a field name, or a method name, consisting of one asterisk. Nothing errors. There is no warning. The grant is simply satisfied by nothing, because no response carries a header named `*` and no request uses a method named `*`. (A fourth field, `Access-Control-Allow-Origin`, also takes `*`, and its behaviour under credentials is a flat refusal rather than a literal reading — the response fails the check outright.) ## What script is left with Without any exposure grant, script can read a fixed set of response header names — the **CORS-safelisted response-header names**: - `Cache-Control` - `Content-Language` - `Content-Length` - `Content-Type` - `Expires` - `Last-Modified` - `Pragma` That is the floor, and a credentialed response granting `*` sits exactly on it. Everything else the server sent is present in the browser and withheld from script. ## The failure this produces A tenant portal pages through maintenance requests and reads a total count from a custom response header. The API's header block carries `Access-Control-Expose-Headers: *`, and during development, before the calls carried the session cookie, it worked perfectly. The day the calls became credentialed, the count became undefined — and every other part of the feature kept working, because the body was still readable. That is the shape to recognise: 1. The response arrives intact; the CORS check passes, since `Allow-Origin` and `Allow-Credentials` are correct. 2. The body reads normally. 3. One custom header is missing from what script can see, and only that. 4. Nothing anywhere reports an error, because nothing failed — a grant was satisfied by nothing. | Field | Credentials mode not "include" | Credentials mode "include" | |---|---|---| | `Access-Control-Expose-Headers: *` | Exposes every header script is allowed to see | Exposes a header named `*` — in effect, nothing | | `Access-Control-Allow-Methods: *` | Clears any method | Clears a method named `*` | | `Access-Control-Allow-Headers: *` | Clears any request header name | Clears a header name that is one asterisk | | `Access-Control-Allow-Origin: *` | Grants any origin | Refused: the check fails outright | ## Why the protocol does this A wildcard is a statement that the resource does not care who reads it. That is a coherent position for a response computed without reference to any user — and an incoherent one for a response computed *for* a signed-in tenant. Rather than add a second syntax for "wildcard, but only for anonymous reads", the protocol makes the existing wildcard inert the moment credentials are in play. The effect is that every credentialed grant is **explicit**: an operator reading the configuration can see exactly which origins, methods and header names were intended. That also means a copied-from-anywhere header block is likely to have been written under the uncredentialed reading, and will quietly narrow the moment the first cookie is attached. ## Getting it right 1. Under credentials, enumerate: list the response header names script actually needs in `Access-Control-Expose-Headers`, and the methods and request header names the preflight must clear. 2. Treat a `*` in a credentialed grant as a defect in review, not as a shorthand — it is indistinguishable from a typo and behaves like one. 3. Remember that a header the browser holds but does not expose is not a server problem: the bytes arrived, and the server has nothing further to do except name them. 4. Expect no error signal. The symptom is a missing value, not a failure. ## What to carry away - `*` is a wildcard in these three fields only for uncredentialed requests. - Under credentials it is a literal name, so the grant matches nothing. - The seven safelisted response-header names remain readable regardless. - `Access-Control-Allow-Origin: *` is a different story: refused, not read literally.

  • The same response is fetched without credentials and the custom header becomes readable. Why?
    Because the wildcard is expanded only when the credentials mode is not `"include"`. Uncredentialed, `Access-Control-Expose-Headers: *` means every header script may see; credentialed, it means a header literally named `*`. The response bytes are identical in both cases — what changed is how the browser reads the grant, which is why the bug appears at the moment a feature starts carrying the session.
  • Is `Access-Control-Allow-Origin: *` also read literally under credentials?
    No, and the distinction is worth keeping straight. That field is not matched literally; a credentialed request whose response grants `*` fails the CORS check outright, and script gets a network error. The other three fields fail silently by matching nothing, which is why they are far harder to notice.

saying these in an interview costs you the question

  • The wildcard exposes every header regardless of credentials
  • A credentialed wildcard grant raises an error the browser reports
  • Only Allow-Origin changes meaning when credentials are involved
  • The server must resend the header for script to read it
  • Exposing headers is about the server sending them, not script reading them