Skip to content
Vulnerabilities

When your API returns more than it shows

Your profile page displays a name and an avatar. The response behind it carries an email address, a phone number, a password reset token and an internal risk score. Nobody sees it, everybody can read it, and no part of using the product would ever reveal it.

7 min read

Key takeaways

  • Filtering in the interface is not filtering. Anything in the response is public to whoever opened developer tools.
  • The cause is almost always returning a database record directly and letting the client choose what to render.
  • It is invisible from inside the product, which is why it survives design review, QA and launch.
  • The fix is a response shape defined per endpoint, allowlisted rather than blocklisted, so a new column is private until somebody decides otherwise.
  • The same root cause runs the other way as mass assignment: a field you did not intend to accept, written because the object was bound wholesale.

How it happens

An endpoint needs to return a user. The quickest correct-looking thing is to fetch the record and serialise it. The front end takes what it needs and renders two fields. Everything works, the page is right, and the response carries the whole row.

It is not laziness so much as the default behaviour of every framework that offers to turn a model into JSON for you. Nothing warns you, because from the framework point of view you asked for the object and got it.

Then the schema grows. Somebody adds an internal flag, a partner identifier, a fraud score, a soft-delete marker. None of them are on any screen and all of them are now in the response, because the serialiser was never told what to include.

Why nobody notices

Every route into the product hides it. Designers see the mockup, developers see the component, QA tests the screen, and the only way to see the problem is to read the raw response of an endpoint nobody has reason to inspect. It takes a person deliberately looking, which is a large part of what an assessment is.

What tends to be in there

  • Contact details for other users, returned wholesale by a search or listing endpoint.
  • Internal identifiers and status flags that describe how you classify a customer.
  • Password reset or verification tokens included on a user object.
  • Soft-deleted records, still returned because the filter was applied in the interface.
  • Fields from a joined table that came along with the relation nobody trimmed.
  • Pricing, margin or cost data on a product object where only the sale price is shown.

The severity depends entirely on the field. A soft-delete flag is noise. A reset token in a response readable by another user is a full account takeover, and the difference between the two is one line in a serialiser.

It also compounds. An identifier leaked here feeds an access control flaw there. On its own it is an information finding; next to an endpoint that does not check ownership it is a bulk export.

The same mistake, running the other way

If the object is bound wholesale on the way out, it is usually bound wholesale on the way in. That is mass assignment: the endpoint accepts a field nobody intended it to accept, because the update takes the request body and applies it to the model.

A profile update that accepts a role, an account tier, a verified flag or a balance is the same design decision seen from the other side. The OWASP API Security Top 10 recognised this in 2023 by merging the two into a single category about property-level authorisation, which is the right framing: both are the application failing to decide, per field, who may read and who may write.

Test both directions at once

When you find an endpoint returning a field the interface does not show, try sending that field back on an update. The same code path that leaked it will often accept it, and the write side is usually the more serious of the two.

Fixing it at the boundary

  1. Define a response shape per endpoint rather than serialising the model. The endpoint states what it returns; the model stops deciding.
  2. Allowlist the fields, never blocklist them. A blocklist is a list somebody has to remember to update, and the failure mode is a new column becoming public silently.
  3. Vary the shape by caller. The fields an administrator sees and the fields a customer sees are different sets, and one endpoint returning the superset is the same bug wearing a role check.
  4. Do the equivalent on input: bind an explicit set of writable fields, not the request body.
  5. Add a test that fails when a response contains a key the contract does not name. It is the only version of this that survives a schema change six months from now.

The last one is what makes the fix permanent. Everything else corrects today instance; a contract test catches the field somebody adds in a hurry next quarter.

Checking it yourself

This needs no tooling beyond the browser you already have open.

  1. Open developer tools on the network tab and use the product normally for a few minutes.
  2. Read the response bodies rather than the screens, starting with anything that returns a user, an order or a list.
  3. For each response, ask which fields appear anywhere in the interface.
  4. Anything left over is exposure. Decide field by field whether it should be there.

Do it on a listing endpoint first. Those return many objects at once and are where a single unnecessary field becomes a dataset.

FAQ

Questions this raises

What is excessive data exposure?

An API response containing fields the interface never displays. The application filters when rendering rather than when responding, so anything in the payload is readable by anybody who opens developer tools or calls the endpoint directly.

Is this the same as broken object property level authorization?

It is half of it. The OWASP API Security Top 10 merged excessive data exposure and mass assignment into one category in 2023, because both come from the same failure: not deciding, field by field, who may read and who may write.

Will a scanner find it?

Rarely, and not usefully. A tool can flag things that look like an email address or a card number in a response, but it cannot know that an internal risk score should not be there, because that requires knowing what the field means and who the caller is.

How serious is it?

Entirely dependent on the field. A soft-delete flag is noise; a password reset token readable by another user is account takeover. It also compounds, because identifiers leaked here make an access control flaw elsewhere far easier to exploit at scale.

How do we stop it coming back?

A contract test that fails when a response contains a key the endpoint does not declare. Fixing the current instances corrects today; only the test catches the column somebody adds next quarter.