Excessive Data Exposure (Ruby)
ID |
excessive_data_exposure_ruby |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
high |
Remediation Effort |
medium |
Family |
API3:2023 - Broken Object Property Level Authorization |
CWE |
CWE-213, CWE-200 |
Resource |
data_exposure |
Language |
Rails, Sinatra |
Description
Walks each endpoint’s response model and classifies every field into one of three confidence tiers, based on whether the field is sensitivity-tagged (PII / PCI / PHI / credentials by the sensitivity classifier) and whether the same field name appears in the request:
-
HIGH — sensitive AND not referenced in the request. The caller never asked for the field; returning it is over-fetch.
-
MEDIUM — sensitive AND referenced in the request. Possibly a legitimate field-by-field update — surfaced for review.
-
LOW — not sensitivity-tagged AND not referenced in the request. Mild signal of over-fetching.
Two kinds of field are never counted. In Java and Python, when the handler returns the authenticated caller’s own record — a "my profile" view that serializes request.user, or looks the user up by getUserPrincipal().getName(), on a route with no path parameter — the caller’s own PII / PHI is not over-exposure (credentials and card data still are). In Python, a DRF write_only field is bound from the request and never serialized, so it is not response content.
Only the HIGH tier fires by default. The detector is per-language because the framework idioms differ — JSON-annotation models in Java / C#, dataclass / Pydantic models in Python, plain-object / class-transformer in JS/TS, struct tags in Go, Eloquent / Symfony serializer groups in PHP.
Rationale
Excessive Data Exposure is the read side of OWASP API3:2023 — the API returns more than the client needs, because the implementation returned a persistence model directly and the persistence model has more fields than the contract. The risk is twofold:
-
Direct data leak — the extra fields contain credentials (
passwordHash,apiToken), PII (email,phone,ssn), or PCI (cardLastFour,accountNumber). Anyone reading the response sees them. -
Object-property authorization bypass — even if the endpoint is authorised to return the object, individual fields on that object should be scoped. A "GET my profile" endpoint authorised for the user should not expose the user’s
isAdminflag,lastLoginIp, orinternalNotes. API3 separates these from API1 (object-level) for exactly this reason.
The pattern is endemic in single-page-app backends, where the same User / Order / Patient entity is reused across all read endpoints, with the client deciding which fields to show. The "client filters out the bad fields" defence does not work — anyone can read the network response.
This rule is the wider one of the pair: it flags a returned field the request never referenced,
whether or not the sensitivity classifier tagged it. Its narrower sibling,
pii_leak_in_response_ruby, fires only on regulated data — see
that page for the Ruby examples, which are the same shape.
The confidence ladder decides what is reported:
-
high — the field is tagged sensitive and the request never mentioned it.
-
medium — the field is tagged sensitive and the request did mention it.
-
low — the field is not tagged, and the request never mentioned it.
minConfidence defaults to high, so only the first tier reports until it is lowered.
Remediation
The fix is to return a dedicated response model per operation, narrow to the fields the operation actually needs to expose. Specific per-language patterns are in the language pages.
Beyond per-route DTOs:
-
If the sensitivity tag is wrong for your project — for example, an email-newsletter app whose
User.emailfield is intentionally public — exclude the field via per-project sensitivity overrides rather than suppressing the finding. -
Pair this detector with
pii_leak_in_response(focuses on PII / PCI / PHI specifically, with stricter severity). -
Server-side response filtering is the only effective control. Anything that depends on the client hiding fields is not a remediation.
The remedy is the one the sibling page shows: a response type that names the fields the operation exposes, mapped explicitly from the model, so widening the model cannot widen the response.
Response model coverage
These two rules compare an endpoint’s response model against its request, so they report nothing
at all on an endpoint whose response model was never established. That is not the same as a clean
result, and the report says which of the two it is: every endpoint carries a responseTypeState of
present, absent (the handler answers with a status and no body) or undetermined (nothing could
establish what it returns). An endpoint with no finding and responseTypeState: undetermined was
not examined by these rules.
Every framework whose endpoints are discovered has its response model read:
| Language | Where the response model is read from |
|---|---|
Java / Kotlin |
Spring MVC and JAX-RS (the handler’s declared return type), Micronaut (same, unwrapping
|
C# |
ASP.NET Core |
JavaScript / TypeScript |
Express, Koa, Fastify and Hono (the response DTO the route resolves); NestJS (the method’s
TypeScript return type); Next.js, Nuxt, SvelteKit and Remix (the object the handler returns, or
the one it hands to |
Python |
Django (incl. DRF), FastAPI, Flask |
Ruby |
Rails ( |
Swift |
Vapor (the route handler’s declared return type, unwrapping |
Go |
net/http, gin, echo, chi and fiber (the payload of the handler’s JSON write, resolved to a struct or read as the map literal it is) |
PHP |
Laravel and Symfony (the JSON response’s argument: an API Resource or transformer’s keys, an
Eloquent model’s |
Any |
An OpenAPI or GraphQL description of the endpoint, where the project ships one |
How often it can be established varies, and depends on how a project writes its handlers rather than on the framework:
-
a declared return type — Micronaut, Vapor, NestJS in TypeScript — resolves nearly always;
-
a response built from a named model, serializer or struct resolves when that declaration is in the scanned source, and not when it comes from a dependency;
-
a response assembled inline — a map, a hash, an object literal — is read as an object of its own keys, which is exact, and is named after the endpoint that returns it;
-
a response produced by a project-specific indirection — a serializer registry keyed by a symbol, a helper that takes the shape as an argument and wraps it — is
undetermined, and says so rather than being guessed at.
One limit applies even where the model is read: a response type declared in a dependency rather than in the scanned source has a name but no fields, so the report shows the type and these two rules have nothing to compare it against.
Configuration
The detector accepts:
-
minConfidence— the minimum confidence tier that fires. Defaulthigh. Set tomediumto include "sensitive AND referenced in request" findings. Set tolowonly for benchmark / audit runs (very noisy).
The set of sensitivity tags (PII / PCI / PHI / credentials) is configured globally on the sensitivity classifier, not per-detector.
References
-
OWASP API Security Top 10 (2023) - API3:2023 - Broken Object Property Level Authorization.
-
OWASP API Security Top 10 (2023) - API10:2023 - Unsafe Consumption of APIs.
-
CWE-213 : Exposure of Sensitive Information Due to Incompatible Policies.
-
CWE-200 : Exposure of Sensitive Information to an Unauthorized Actor.
-
OWASP Cheat Sheets Series: REST Security Cheat Sheet — section on minimum information disclosure.