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.
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:
- 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
Originit is given, or that matches a subdomain wildcard, has handed its trust boundary to every host under the domain. - Never combine a reflected or wildcard origin with credentials.
Access-Control-Allow-Credentials: truealongside 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. - 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.
- 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?
Why is reflecting arbitrary subdomains with credentials dangerous?
How do you fix a wildcard CORS with credentials issue?
This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.
How it works →

