Excessive Data Exposure (C#)
ID |
excessive_data_exposure_csharp |
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 |
ASP.NET Core (Controllers + Minimal APIs) |
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.
A susceptible ASP.NET Core controller might look like this:
[ApiController]
[Route("users")]
public class UsersController : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<ActionResult<User>> Get(int id)
=> await _db.Users.FindAsync(id) is { } u ? Ok(u) : NotFound();
}
public class User
{
public int Id { get; set; }
public string Username { get; set; } = "";
public string Email { get; set; } = ""; // PII
public string PasswordHash { get; set; } = ""; // CREDENTIAL
public string Ssn { get; set; } = ""; // PII
public bool IsAdmin { get; set; } // PRIVILEGE
}
The handler returns the EF Core entity, so every property is serialised into the response.
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.
Here is a revised handler returning a dedicated ViewModel:
public record PublicUser(int Id, string Username);
[ApiController]
[Route("users")]
public class UsersController : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<ActionResult<PublicUser>> Get(int id)
=> await _db.Users
.Where(u => u.Id == id)
.Select(u => new PublicUser(u.Id, u.Username))
.FirstOrDefaultAsync() is { } p ? Ok(p) : NotFound();
}
The projection (Select(…)) shapes the response and removes the sensitive columns from the SQL query itself — defence in depth.
Other C# idioms:
-
[JsonIgnore]on each sensitive property — fragile (every new sensitive field must be annotated). -
JsonSerializerOptionsdefaults that hide properties matching a regex. -
AutoMapper profiles that map
UsertoPublicUserconsistently.
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.
-
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.
-
Microsoft docs — Choose between controller-based APIs and Minimal APIs in ASP.NET Core.
-
Microsoft docs — Controller action return types.