At a glance
- Controls
- OWASP API3:2023 · SOC 2 CC6.1 · ISO 27001 A.8.2
A response is only as private as its narrowest field. Products often send the whole database row to the browser and let the interface pick what to show. What the user sees is a tidy profile card; what the response contains is the full record, including fields the design never intended to expose. Hiding a field in the interface does nothing to the bytes on the wire.
Why it happens
The fastest way to build an endpoint is to serialise the object and return it. An assistant asked for a profile endpoint returns the profile object, all of it, because trimming it to the fields the screen needs is extra work nobody specified. As the record grows a new column at a time, each addition quietly joins the response, and the endpoint that once returned a name now returns a name, an email, a password reset token and an internal role.
How the attack unfolds
An attacker opens the network tab and reads the responses the product already sends them. No special access is required, because the data is being handed over on every page load. They look for identifiers to reuse, tokens to replay, email addresses to harvest and role flags to flip, and they check whether the list endpoint returns other people’s records with the same generous shape.
Nobody exfiltrated the data. The product delivered it, formatted, on every request.MATT research team
Why review misses it
The page looks right, so review signs off. The extra fields are invisible unless someone reads the raw response, and the endpoint that leaks the most is often the one that felt too simple to examine. The defect lives in what the response includes, and the interface is designed to make that invisible.
Where it hides
What a reviewer can look for in the code and in the behaviour, before anyone proves it.
- Endpoints serialise and return whole records, with no explicit list of allowed fields.
- Fields hidden in the interface are still present in the response body.
- List endpoints return the same broad shape as detail endpoints, multiplied across records.
- Tokens, internal identifiers or role flags appear in responses that the screen never shows.
A real instance
Details altered to protect the customer. Pattern, timing and outcome are as found.
How MATT tests it
The proof runs against the deployed product, on every release, whoever wrote the code.
- 01Read every response in the network tab and compare its fields against what the screen displays.
- 02Look for identifiers, tokens, email addresses and role flags that the interface never renders.
- 03Call list and search endpoints and check whether other people’s records arrive with the same broad shape.
- 04Test whether a hidden field can be acted on: a returned token replayed, a role flag flipped on write.
The evidence
- GET /api/profile200 OKReturns name, avatar, email, internal id, role, reset token
- GET /api/users?team=acme200 OKEvery teammate’s full record, same broad shape
- GET /api/profile200 OKReturns only the fields the screen displays
- GET /api/users?team=acme200 OKPublic fields only, chosen explicitly per endpoint
A response must contain only the fields the screen displays.
Executable checkEvery endpoint’s response must validate against an explicit field allow-list, with no token, secret or internal identifier present, on every release.
Every rule MATT holds →What the fix looks like
- 01
Return an allow-list, not the record
Serialise an explicit set of fields per endpoint, so a new column joins a response only when someone adds it on purpose.
- 02
Separate read models from storage
Shape responses for the screen that consumes them, so internal fields never have a path to the client.
- 03
Pin it with a rule
A response must contain only the fields its screen displays, checked against a schema on every release.
A check that was never written is found by proving the deployed behaviour, fixed with the engineer who shipped it, and kept fixed by running the control on every change. What is ExploitOps? →