Back to Cheat Sheets

📤 Excessive Data ExposureAPI 2019

OWASP API Security Top 10 2019 - API3

HIGH RISK

📋 What Is It?

Excessive Data Exposure is what happens when an API returns more data than the client needs and trusts the client to hide the surplus. The server serialises a whole internal object - every column of a row, every attribute of a model - and the UI displays only a handful of fields. On screen everything looks fine; in the raw JSON the hidden fields are right there for anyone with dev tools, a proxy, or curl.

The defining characteristic is that the filtering happens in the wrong place. The endpoint is authorised - the caller may call it - but the response is over-shared, leaking properties this consumer was never meant to see.

API3 OWASP Rank 2019
Response Where It Leaks
PII Typical Payload

⚠️ Common Exploits

  • Whole-Model Serialisation: to_dict() / toJSON() ships every column
  • Secrets in Responses: password hashes, MFA seeds, reset tokens, internal keys
  • Hidden-in-the-UI Fields: email, phone, DOB, exact coordinates behind a "2 mi away" label
  • Internal Flags: isAdmin, risk scores, feature toggles, billing identifiers
  • Over-Eager Relations: nested objects pulled in by ORM eager loading
  • Verbose Errors & Debug: stack traces and full objects in error bodies
  • GraphQL Over-Fetch: the schema exposes fields the resolver never filters

🔴 Attack Flow

1. Use the app normally with an intercepting proxy attached
↓
2. Compare what the UI renders with the raw JSON it received
↓
3. Spot fields the interface never shows
↓
4. Call the endpoint directly and keep everything the client discards
↓
5. IMPACT: Harvest PII and secrets at scale - from an "authorised" call!

❌ Vulnerable Code

# Bad: the whole model goes out the door @app.route('/api/users/<int:uid>') def get_user(uid): user = User.query.get(uid) return jsonify(user.__dict__) ← password_hash, mfa_secret, is_admin, … // Bad: "the app only shows the name" - the wire says otherwise res.json({ id: user.id, displayName: user.displayName, avatarUrl: user.avatarUrl, email: user.email, phone: user.phone, dateOfBirth: user.dob, ← PII, never rendered passwordHash: user.passwordHash, mfaSecret: user.mfaSecret, ← must never leave the server isAdmin: user.isAdmin, internalRiskScore: user.risk, ← internal logic preciseLat: user.lat, preciseLng: user.lng ← UI shows "2 mi away" }); // Bad: filtering delegated to the front end const visible = (await api.get('/users/42')).data; render(visible.displayName); ← the rest is still in the browser, and in the response

✅ Secure Code

# Good: explicit allow-list per consumer, defined once PUBLIC_FIELDS = ('id', 'display_name', 'avatar_url') OWNER_FIELDS = PUBLIC_FIELDS + ('email', 'phone') @app.route('/api/users/<int:uid>') @requires_auth def get_user(uid): user = User.query.get_or_404(uid) fields = OWNER_FIELDS if uid == g.current_user.id else PUBLIC_FIELDS return jsonify({f: getattr(user, f) for f in fields}) // Good: a typed response DTO - new columns are not auto-exposed function toPublicUser(u) { return { id: u.id, displayName: u.displayName, avatarUrl: u.avatarUrl }; } res.json(toPublicUser(user)); # Good: reduce precision instead of shipping raw values resp['distance_band'] = band(haversine(me, them)) # "2 mi", not lat/lng // Good: schema-enforced responses (OpenAPI response validation) app.use(validateResponses({ removeAdditional: 'all' })); // undeclared fields stripped

✓ Prevention Checklist

  • Never serialise a database model straight to the response
  • Define an explicit allow-list (DTO/schema) per endpoint and per audience
  • Classify data - PII, secrets, internal flags - and gate each class
  • Validate responses against an OpenAPI/JSON schema and strip extras
  • Return the least precision that satisfies the use case
  • Review generic "toJSON" helpers, ORM eager loading, and GraphQL resolvers
  • Keep debug output and stack traces out of production responses
  • Treat every new column as opt-in, not opt-out
  • Test the raw response, not the rendered screen

🔍 Detection & Tools

Inspection:

Burp Suite mitmproxy curl / httpie Browser dev tools

Enforcement:

OpenAPI validators Marshmallow / Pydantic DTO mappers DLP response scanners

How to Test:

  • Diff every rendered field against the raw JSON body
  • Grep responses for password, secret, token, ssn, lat
  • Call endpoints as an anonymous, a normal, and a privileged user and diff
  • Query GraphQL introspection for fields the UI never requests
  • Trigger errors and read what the error body contains

🌍 Real-World

These are documented incident classes - recurring patterns rather than single CVEs.

  • Dating and social apps leaking coordinates: the UI shows a distance band while the API returns exact latitude and longitude, enabling trilateration to a doorstep.
  • Profile endpoints returning credentials material: password hashes and MFA seeds included in a user object because the model was serialised wholesale.
  • Internal flags exposed: risk scores, tiers, and isAdmin booleans revealed in ordinary responses, mapping the app's authorization model for an attacker.
  • Partner integrations over-sharing: a single "user" payload reused for a third-party integration ships fields the partner should never receive.

📌 Quick Tips

  • DO NOT rely on the client to hide fields
  • DO NOT return model objects directly
  • DO allow-list fields per endpoint and per role
  • DO validate responses against a schema
  • DO lower precision on sensitive values

📜 Compliance

Related Standards:

  • OWASP API Security Top 10 2019 - API3
  • Merged into API3:2023 Broken Object Property Level Authorization
  • CWE-213 Exposure of Sensitive Information Due to Incompatible Policies
  • CWE-359 Exposure of Private Personal Information
  • GDPR Art. 5(1)(c) Data Minimisation