Home/Journal/Teardown
TeardownHighcorsxssauthentication

When the Trust Boundary Is the Whole Domain

A wildcard subdomain CORS policy turned one weak subdomain into a domain-wide data breach. Here is how we found it and how to close it.

Rahul Dharan··8 min read·a durable-medical-equipment marketplaceOverly permissive CORS with credentials (CWE-942)

The setup

The target was a durable-medical-equipment marketplace: the software layer that connects patients, suppliers, and insurance carriers so that a wheelchair, a CPAP machine, or a home oxygen concentrator can be ordered, billed, and delivered. That kind of platform sits on a pile of sensitive records. Patient shipping addresses. Supplier tax identities. Insurance-carrier contracts. It is a business built on many parties trusting one API to hold their data and hand it back only to the right person.

Multi-party marketplaces like this tend to grow a lot of subdomains: a customer app, a supplier portal, an admin console, a status dashboard, a marketing site, and a few that were stood up for a launch and never taken down. That sprawl is what we wanted to understand, because in a system with many front doors the interesting question is always whether they share one lock.

Recon

Working black-box, with no account, we enumerated the company’s subdomains and mapped which ones answered and what each one was. There were the expected ones - the main application, an API host, a supplier area. There were also older hosts: a dashboard component that looked like it had not been touched in a while, running a version of a common front-end library old enough to have a published vulnerability against it.

We then looked at the API itself. We watched how it answered cross-origin requests, because the way an API talks to browsers on other origins tells you where it thinks its trust boundary is. That is where the shape of the finding first appeared.

The hypothesis

Two observations lined up.

First, the API set permissive cross-origin headers. When we sent a request with an arbitrary Origin, the API reflected certain origins straight back in Access-Control-Allow-Origin and paired that with Access-Control-Allow-Credentials: true. The pattern it accepted was any subdomain of the company’s own domain.

Second, one of those subdomains was running an outdated dashboard component with a known cross-site-scripting flaw.

Put together, the hypothesis was uncomfortable. If the API trusts every *.company origin with credentials, then the trust boundary is not any single service. It is the whole domain. And a domain is only as trustworthy as its weakest subdomain. A cross-site-scripting flaw on that one stale dashboard would be running on an origin the API trusts, which means it could read authenticated API responses for any logged-in user - not just data on the dashboard, but data across the entire product.

What we tried

We confirmed the pieces separately, and we were deliberate about where we stopped.

We verified the CORS reflection directly: we sent requests with various Origin values and read the response headers. Origins that matched the subdomain pattern came back reflected, with credentials allowed. Origins outside it did not. So the reflection was real, and it was scoped to the whole domain rather than to a named list of apps.

We confirmed the outdated component and its known cross-site-scripting flaw on the stale subdomain by fingerprinting the version and matching it to the public advisory. The primitive was present.

What we did not do was chain the two into a live theft. Running the full end-to-end path means driving a cross-site-scripting payload on a real user’s session to pull their authenticated data out through the trusted origin. That crosses from proving a door is open into walking a real person’s data through it. We proved each primitive - the CORS reflection, the reachable cross-site-scripting flaw, and the fact that the trusted origin could read authenticated responses - and stopped there. The chain was verified as components, not executed against a live session.

What we found

Two findings, one of them a chain and one of them standalone.

The chain was the trust-boundary collapse. The API reflected any subdomain origin and allowed credentials, so any script running on any subdomain of the company could ask the browser to make authenticated calls to the API and read the answers. Because one subdomain carried a known cross-site-scripting flaw, an attacker had a place to run that script. The mechanism, in shape:

Request from the stale subdomain's origin:
  GET /<authenticated-account-endpoint>
  Origin: https://<stale-subdomain>.<company>
  Cookie: <the victim's session cookie, sent by the browser>

Response:
  Access-Control-Allow-Origin: https://<stale-subdomain>.<company>
  Access-Control-Allow-Credentials: true
  { ... the victim's authenticated account data ... }

The API answered as if the stale subdomain were a first-class part of the product, because as far as its CORS policy was concerned, it was.

The standalone finding was simpler and did not need any chain at all. A separate API returned supplier and insurance-carrier records with no access control: business names, tax identification numbers (EINs), full contact details, and addresses, served to an anonymous caller. That one was a straight unauthenticated read of a directory that should have sat behind authentication.

Why it happens

Nobody set out to trust a forgotten subdomain. Wildcard subdomain CORS is what you reach for when you have several apps under one domain and you are moving fast. Writing *.company into the policy, or reflecting any subdomain origin, means every new app you launch just works with the API on day one. You never have to come back and add it to a list. Under deadline pressure, that convenience is the whole appeal.

The cost is invisible at the time and shows up later. The policy quietly promises that every subdomain you will ever run is as trustworthy as your most careful one. Then a dashboard gets stood up for a launch, the team moves on, its dependencies age, and a published vulnerability lands against a component it still runs. The subdomain was never decommissioned, so it is still inside the trust boundary the CORS policy drew around the entire domain. The convenient policy and the forgotten host were shipped by different people at different times, and neither could see the other.

For developers

If your API serves browsers across several subdomains, these controls close this:

  1. Allow-list exact origins. Never reflect arbitrary ones. Maintain a named list of the exact origins allowed to make credentialed requests, and compare against it. An API that echoes back whatever Origin it is given, or that matches a subdomain wildcard, has handed its trust boundary to every host under the domain.
  2. Never combine a reflected or wildcard origin with credentials. Access-Control-Allow-Credentials: true alongside a reflected origin is the specific combination that turns a CORS quirk into an authenticated-data read. If you need credentials, you need an explicit, short, exact allow-list.
  3. Patch every subdomain, or retire it. With a permissive CORS policy, every subdomain shares one trust boundary, so a cross-site-scripting flaw on the least-maintained host is a flaw against your authenticated API. Inventory your subdomains, patch their components, and take down the ones you no longer use.
  4. Put access control on directory endpoints. Supplier, carrier, and partner records are not public data. An endpoint that returns names, tax IDs, and addresses needs authentication and authorization in front of it, regardless of how the front-end reaches it.

One detection step: from an outside origin, send your own API a credentialed cross-origin request and read the response headers. If it reflects your made-up origin and allows credentials, your trust boundary is wider than you think.

The takeaway

A wildcard CORS policy does not just relax a rule on one endpoint. It redraws the trust boundary around your whole domain, and a domain is only as trustworthy as its weakest subdomain. The moment credentials are allowed for any subdomain, a single stale host with a known flaw becomes a way to read authenticated data across the entire product. The fix is to name the origins you trust, keep that list short, and remember that every subdomain you never took down is still inside the circle you drew.

This teardown is one instance of a broader pattern - data that should sit behind a lock, reachable because a boundary was drawn too wide. We wrote up the general case, and how to defend against it, in The Cloud Database With the Door Left Open.

Frequently asked

What is an overly permissive CORS policy with credentials?
It is a cross-origin resource sharing configuration that reflects the request's origin back in the Access-Control-Allow-Origin header while also setting Access-Control-Allow-Credentials to true. When the reflection accepts any subdomain of a domain, any page on any subdomain can read authenticated API responses on behalf of a logged-in user.
Why is reflecting arbitrary subdomains with credentials dangerous?
It collapses the trust boundary from a single service to the entire domain. A cross-site-scripting flaw on any one subdomain, even a forgotten dashboard, can then read authenticated data across every other subdomain, because the API trusts them all equally.
How do you fix a wildcard CORS with credentials issue?
Replace origin reflection with an explicit allow-list of exact origins, never combine a wildcard or reflected origin with Access-Control-Allow-Credentials, and patch or retire vulnerable components on every subdomain, since a permissive CORS policy makes them share one trust boundary.

This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.

How it works →