Excessive Data Exposure (Kotlin)

ID

excessive_data_exposure_kotlin

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

Spring Boot (Kotlin), Ktor, Micronaut

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 isAdmin flag, lastLoginIp, or internalNotes. 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_kotlin, fires only on regulated data — see that page for the Kotlin 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.email field 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 HttpResponse / Mono / Single), Ktor (the call.respond… argument, through the receiver’s declared type and the callee’s declared return type)

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 json(…))

Python

Django (incl. DRF), FastAPI, Flask

Ruby

Rails (render json: → serializer, else the model’s columns in db/schema.rb), Sinatra (the block’s value, including a hash it builds before serialising it)

Swift

Vapor (the route handler’s declared return type, unwrapping EventLoopFuture and arrays)

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 $fillable minus its $hidden, or the array literal returned)

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. Default high. Set to medium to include "sensitive AND referenced in request" findings. Set to low only 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

© 2026 Xygeni. All rights reserved