CORS Origin Reflection with Credentials Enabled
Origin reflection — where a server reads the Origin header from the
incoming request and echoes it back in Access-Control-Allow-Origin —
appears regularly in production, usually because someone was solving a CORS problem
quickly and landed on the worst possible solution. Found on a
web application's user profile endpoint: the server reflected any submitted origin
back in its CORS headers and included Access-Control-Allow-Credentials: true.
Any website on the internet could make authenticated cross-origin requests to the
profile endpoint on behalf of a visiting user and read the full response.
The detection
A single proxy modification: add Origin: https://evil.lab.attacker.test
to any authenticated API request and inspect the response headers. A correctly configured
server ignores or rejects origins not on its allowlist. This server reflected the
submitted value back with Access-Control-Allow-Credentials: true:
GET /api/profiles/49 HTTP/1.1
Host: app.acme-lab.test
Cookie: JSESSIONID=abc123lab; XSRF-TOKEN=def456lab
Origin: https://evil.lab.attacker.test
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://evil.lab.attacker.test
Access-Control-Allow-Credentials: true
{"userId":49,"name":"Lab User","email":"lab@acme-lab.test","role":"analyst"}
# origin reflected verbatim — credentials allowed
# any origin can now read authenticated responses for visiting users
What it enables
A page at any attacker-controlled origin can use fetch() with
credentials: 'include' to make an authenticated request to the profile
endpoint on behalf of a user who visits the page. The browser sends the victim's session
cookies automatically. The server's reflected CORS header tells the browser the
attacker's origin is permitted to read the response. The profile data — name, email,
role, account identifiers — is returned and readable by the attacker's JavaScript.
The victim sees nothing.
The endpoint used a sequential integer path parameter: /api/profiles/49.
The CORS misconfiguration combined with predictable IDs means an attacker's page can
iterate through IDs and read every accessible profile in a single visit — no interaction
beyond the victim landing on the page.
The test timeline
Arbitrary Origin header added to authenticated request. Standard technique: intercept any authenticated API request, add Origin: https://evil.lab.attacker.test, forward, inspect response headers.
Response reflects submitted Origin with credentials=true. Access-Control-Allow-Origin: https://evil.lab.attacker.test and Access-Control-Allow-Credentials: true in response. SOP bypass confirmed.
Integer path parameter identified. CORS misconfiguration combined with sequential IDs creates a cross-origin enumeration path. All profiles accessible to any authenticated user who visits an attacker-controlled page.
Finding documented as Medium. Proof-of-concept fetch with credentials:include returning authenticated profile data. Enumeration chain documented. No enumeration performed beyond a single confirmed read of the test account.
Fixing the policy
- Maintain an explicit, static allowlist of trusted origins in application configuration. Never read the
Originheader from the request and echo it back. If the submitted origin is in the allowlist, respond with that specific value; if it is not, omit the CORS headers entirely. - Be extra conservative on authenticated, user-specific endpoints. A wildcard CORS policy on a public read-only endpoint may be intentional. A wildcard or reflected ACAO on an authenticated profile endpoint is almost certainly wrong — the two cases warrant separate configuration.
- Audit CORS configuration across all API routes after applying a fix. Misconfiguration is often inconsistent. After patching, test every authenticated endpoint with an arbitrary
Originheader and verify the response does not reflect it. - Replace sequential integer IDs with UUIDs on sensitive endpoints. A CORS misconfiguration on an endpoint with sequential integers exposes every record in the database. The two fixes are independent — CORS must be fixed regardless — but UUIDs remove the enumeration amplifier.
The takeaway
The check is one header addition in an intercepting proxy. Add
Origin: https://arbitrary.example to any authenticated request. If the
response reflects it with Access-Control-Allow-Credentials: true, the
CORS policy is broken — the browser will send cookies cross-origin to any site that
asks.