← Back

API3:2019 Excessive Data Exposure - Overview

What is Excessive Data Exposure?

Excessive Data Exposure is what happens when an API returns more data than the specific client actually needs, and trusts the client to filter, hide, or discard the surplus. The server serialises a whole internal object—every column of a database row, every attribute of a domain model, every nested relation—and ships it over the wire. The user interface then displays only a handful of those fields, so on screen everything looks fine. But the raw response tells a very different story: it contains the fields the UI hides, and an attacker who reads that raw JSON simply keeps the parts the app throws away.

The defining characteristic of API3:2019 is that the filtering happens in the wrong place. Presentation-layer code (a mobile app, a single-page web front end, a partner integration) is treated as a trusted security boundary. It is not. Anyone can inspect network traffic with browser dev tools, an intercepting proxy such as Burp or mitmproxy, or by pointing curl at the endpoint directly. The moment data leaves the server it is fully visible, regardless of what the client chooses to render.

In one sentence: the endpoint is authorised—the user is allowed to call it—but the response is over-shared, leaking properties that this consumer was never meant to see.

Core Concept

What the mobile app SHOWS on the profile screen:
  {
    "id": 42,
    "displayName": "Jordan Lee",
    "avatarUrl": "https://cdn.example.com/u/42.png"
  }

What the API actually RETURNS for GET /api/users/42:
  {
    "id": 42,
    "displayName": "Jordan Lee",
    "avatarUrl": "https://cdn.example.com/u/42.png",
    "email": "jordan.lee@example.com",       <- PII, not shown in UI
    "phone": "+1-202-555-0142",               <- PII, not shown in UI
    "dateOfBirth": "1991-03-08",              <- PII, not shown in UI
    "passwordHash": "$2b$12$Q8f...",          <- secret, must NEVER leave server
    "mfaSecret": "JBSWY3DPEHPK3PXP",          <- secret
    "isAdmin": false,                          <- internal authorization flag
    "internalRiskScore": 0.83,                <- internal business logic
    "stripeCustomerId": "cus_Nf1...",          <- internal 3rd-party identifier
    "preciseLat": 47.61032,                    <- exact location, UI shows "2 mi away"
    "preciseLng": -122.33207
  }

The client rendered three fields. The server sent thirteen. The extra ten—PII, secrets, internal flags, and precise coordinates—are the vulnerability. No exploit chain is required; the attacker just reads the response body.

Why It's Specific to APIs

In a classic server-rendered web application, the server builds the HTML and only the finished page reaches the browser. Fields the template does not print are never transmitted. Modern APIs invert this: the server emits structured data and delegates rendering to the client. That architectural shift is exactly what makes Excessive Data Exposure an API-native problem:

Why Does This Matter?

Business Impact

Technical Impact

Technical Context

How the Anti-Pattern Arises

1. Returning the Entire Database Object / Serialized Model

# Flask + SQLAlchemy: the whole row, verbatim
@app.route('/api/users/<int:uid>')
def get_user(uid):
    user = User.query.get_or_404(uid)
    return jsonify(user.to_dict())   # to_dict() dumps EVERY column,
                                     # including password_hash, mfa_secret, is_admin

The developer wanted "the user," the ORM offered a one-line dump of the row, and every sensitive column rode along.

2. Over-Broad Generic Serializers

# Django REST Framework: fields = '__all__' returns everything
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = '__all__'   # deny-list mindset: expose all, hope to hide later

__all__ is a deny-list in disguise: every current and future column is exposed by default, so adding a sensitive field later silently leaks it.

3. SELECT * Flowing Straight to the Response

SELECT * FROM users WHERE id = $1;   -- grabs every column
-- ...and the handler serialises the whole row to JSON unchanged

4. Verbose Nested Relations

GET /api/orders/1001
{
  "id": 1001,
  "total": 59.90,
  "customer": {                 // full customer object embedded
    "id": 42, "email": "...", "passwordHash": "...", "internalNotes": "..."
  },
  "payment": {                  // full payment object embedded
    "cardLast4": "4242", "cardToken": "tok_...", "gatewayCustomerId": "cus_..."
  }
}

Eager-loading relations for convenience embeds entire adjacent objects—each with its own sensitive fields.

5. Debug / Internal Fields Left In

{ "id": 7, "title": "Hello",
  "_debug": { "sqlQuery": "SELECT ...", "cacheKey": "u:7:v3", "featureFlags": {...} },
  "__v": 12, "deletedAt": null, "tenantId": "acme-internal" }

Where the Filtering Should Happen

ApproachWho decides which fields shipSecure?
Client hides fields in the UIThe client (untrusted)No — raw response still contains everything
Server returns full model, docs say "ignore extra fields"Nobody enforces itNo — hope is not a control
Server returns an explicit response DTO / schema (allow-list)The server (trusted)Yes — only named fields can ever leave
Server shapes the response per role/consumerThe server (trusted)Yes — each caller gets only what it needs

Data That Should Almost Never Leave the Server

Real-World Impact

The following are well-established classes of incident. They describe recurring, publicly discussed patterns rather than any single named CVE, and no precise figures are asserted.

Case Class 1: Mobile / SPA Backends Returning Full User Objects

Pattern: A mobile or single-page app shows a trimmed profile, but its backend endpoint returns the complete user record. Security researchers repeatedly demonstrate this by intercepting the app's own traffic with a proxy and observing fields—email, phone, date of birth, internal flags—that never appear on screen.

Impact: Bulk PII harvesting and, where hashes or tokens are included, a path to account takeover.

Root Cause: The server serialised the whole model and trusted the app to display a subset.

Case Class 2: Precise Location Leaked Behind "Approximate Distance"

Pattern: Location-based social and dating apps display a coarse "2 miles away," but the underlying API response has historically included exact latitude/longitude (or a precise-enough distance to trilaterate). This class of finding has been reported multiple times across several apps by independent researchers.

Impact: An attacker can pinpoint a specific user's real-world location—a serious physical-safety issue—without the app ever displaying a map pin.

Root Cause: Coarsening was applied in the UI, while the API returned precise coordinates.

Case Class 3: Over-Sharing List and Search Endpoints

Pattern: A "list users," "search," or "directory" endpoint returns full objects for every result. The caller is authorised to list, but each element carries sensitive fields for other people.

Impact: A single request enumerates sensitive properties across the whole result set—one call becomes a mass scrape.

Root Cause: The same maximal serializer used for a single object was reused for collections.

Case Class 4: GraphQL / Flexible-Query Over-Fetching

Pattern: A schema exposes sensitive fields on a type, relying on the client not to request them. Because clients choose their own field set, an attacker simply asks for the sensitive fields directly.

Impact: Direct disclosure of any field present in the schema but not access-controlled at the field level.

Root Cause: Field-level authorization was assumed to be a client concern rather than a server control.

Prevalence and Detectability

In the OWASP API Security Top 10 (2019), Excessive Data Exposure was ranked API3 and characterised as common, easy to exploit, and moderate-to-severe in impact. OWASP notes that detection typically requires inspecting responses rather than source code—because the vulnerability lives in what the server sends, not in a crash or an error.

Edition note (2019 → 2023): In the 2023 edition of the OWASP API Security Top 10, Excessive Data Exposure was merged with Mass Assignment into a single category, API3:2023 – Broken Object Property Level Authorization (BOPLA). The reframing recognises that reading properties you shouldn't (Excessive Data Exposure) and writing properties you shouldn't (Mass Assignment) are two sides of the same missing control: property-level authorization. This lesson uses the 2019 framing and terminology, but the defences below map directly onto the 2023 category.

Common Misunderstandings

Myth 1: "The app doesn't display it, so it's not exposed"

Reality: The response body is fully visible in dev tools, proxies, logs, and curl. What the UI renders is irrelevant to what the API transmits.

Myth 2: "It's fine because the endpoint requires authentication"

Reality: Excessive Data Exposure is about which fields an authenticated, authorised caller receives. Being logged in does not entitle you to another user's password hash or precise location.

Myth 3: "We'll just document that clients should ignore extra fields"

Reality: Documentation is not a security control. If the field is in the payload, it is exposed—compliant clients and attackers receive identical bytes.

Myth 4: "HTTPS protects the data"

Reality: TLS protects data in transit from third parties. It does nothing about the legitimate recipient—the attacker—reading the response their own client received.

Myth 5: "A deny-list of sensitive fields is enough"

Reality: Deny-lists fail open. The day someone adds ssn or internalToken to the model, it leaks until somebody remembers to blacklist it. Allow-lists fail closed and are the only robust approach.

Myth 6: "This is the same as Broken Object Level Authorization (BOLA)"

Reality: They are related but distinct. BOLA (API1) is about accessing an object you shouldn't (wrong id). Excessive Data Exposure is about receiving properties you shouldn't within an object you are allowed to access. You can have one without the other.

How Excessive Data Exposure Differs from Related Issues

AspectExcessive Data Exposure (API3:2019)BOLA (API1:2019)Mass Assignment (API6:2019)
DirectionServer → client (reading)Access to whole objectClient → server (writing)
QuestionWhich fields come out?Whose object is it?Which fields go in?
FailureOver-shared responseMissing object ownership checkBlindly binding request to model
FixExplicit response allow-listPer-object authorizationExplicit input allow-list
2023 mappingAPI3:2023 BOPLAAPI1:2023 BOLAAPI3:2023 BOPLA

Self-Assessment

Ask these questions about each API response your service returns:

If you answered "no" or "not sure" to several of these, your API is very likely over-sharing today.

Key Takeaways

  1. Filtering must happen on the server—the client is not a security boundary, and the raw response is always readable.
  2. Return only what each consumer needs—explicit response DTOs/schemas, defined as allow-lists, not the whole model.
  3. Allow-list, never deny-list—so new fields fail closed instead of leaking by default.
  4. List endpoints multiply the damage—one over-shared collection call can scrape sensitive data at scale.
  5. Review the bytes, not the screen—inspect actual responses; the UI hides the problem from you as effectively as it hides it from no one.

Next Steps