How to Protect ASP.NET Core APIs Against Sensitive Data Exposure

Sensitive data exposure in ASP.NET Core APIs is one of the most frequently overlooked security risks in production systems โ not because developers are unaware of it, but because the leak is almost always accidental. A property gets added to an entity class, serialisation picks it up automatically, and suddenly a password hash, an internal flag, or a user's date of birth is sitting in the API response for anyone with a proxy to read. The OWASP API Security Top 10 lists this class of problem as API3: Excessive Data Exposure โ and it consistently appears in real-world penetration test reports for .NET teams.
The production-ready patterns for tackling this โ response DTO design, serialisation control, output filtering, and logging hygiene โ are covered in depth on Patreon, with annotated source code that maps directly to what enterprise teams actually ship.
Understanding where the problem lives is also inseparable from how your API is designed as a whole. The Zero to Production course covers REST API design with proper request/response DTOs and the Chapter 2 principle that every response shape should be a deliberate contract โ not an accidental projection of your domain model. The course also covers global exception handling in Chapter 6 and authorization boundaries in Chapter 8, both of which directly affect what data leaks and when.
What Is Sensitive Data Exposure in a Web API?
Sensitive data exposure occurs when an API returns more information than the client legitimately needs or is authorised to see. This is distinct from a data breach caused by external attack โ the data is being returned by your own application logic, often without any exploit at all.
Common examples in ASP.NET Core APIs include:
Password hashes or security stamps being serialised from
IdentityUser-derived entitiesInternal audit fields (
CreatedBy,ModifiedBy,DeletedAt) leaking to public endpointsRole flags and permission bitmasks exposing business logic to frontend clients
Personally Identifiable Information (PII) such as full date of birth or national ID numbers returned to roles that only need partial data
Stack traces and exception messages appearing in error responses โ even in production
Database IDs and internal entity keys revealing infrastructure details to external clients
Third-party API keys or connection metadata embedded in response objects during debugging and never removed
The root cause in almost every case is the same: the API response was derived too directly from the domain model or EF Core entity, rather than from a purpose-built response DTO.
Why Developers Get This Wrong
The temptation is real. When building a new endpoint, mapping your entity directly to the response is the path of least resistance. You get the data out quickly, the tests pass, and there are no obvious errors. The problem is invisible โ until a security audit or a penetration test surfaces a passwordHash field sitting in a JSON response that was faithfully serialised from the ApplicationUser object.
Three patterns drive most sensitive data exposure incidents in ASP.NET Core:
1. Returning entities directly from controllers. When an EF Core entity is returned as the HTTP response, every property the ORM loaded ends up in the serialised output. This is the single most common cause. Even if [JsonIgnore] is applied to some properties, the protection is fragile โ any future property addition to the entity is exposed by default.
2. Relying on [JsonIgnore] as a security boundary. [JsonIgnore] tells the serialiser to skip a property, but it does not prevent the property from being loaded by EF Core, passed between layers, or logged. It also creates a tight coupling between your security decisions and your serialisation configuration. When security requirements change (e.g., a field should be visible to admins but hidden from regular users), [JsonIgnore] cannot express that distinction at all.
3. Returning identical response shapes across all caller roles. Enterprise APIs typically serve multiple caller personas โ a mobile client, an admin dashboard, a third-party partner. Returning the same response object to all of them is convenient, but it means the lowest-privilege caller sees everything the highest-privilege caller needs.
The Correct Architecture: Dedicated Response DTOs
The architectural fix is straightforward and should be non-negotiable in any production ASP.NET Core API: never return domain entities or EF Core entities directly from endpoints. Every endpoint should return a purpose-built response DTO, and that DTO should contain exactly and only what the caller is authorised to see.
This is not about defensive coding โ it is about making the API's output a deliberate, reviewable contract rather than an accidental projection of your internal model.
A practical approach is to define separate response DTOs for different caller roles:
// Public-facing โ minimal fields, no internal identifiers
public record ProductSummaryResponse(
Guid PublicId,
string Name,
decimal Price,
string Category);
// Admin-facing โ richer data for internal tooling
public record ProductAdminResponse(
Guid PublicId,
string Name,
decimal Price,
string Category,
bool IsActive,
string CreatedBy,
DateTimeOffset CreatedAt);
Notice what is absent from ProductSummaryResponse: no database integer ID, no audit trail, no status flags. The mapping from your domain entity to either DTO is done explicitly โ every field is a deliberate choice.
For the mapping itself, a simple manual projection (Select(e => new ProductSummaryResponse(...))) or a library like AutoMapper or Mapperly is sufficient. The important principle is that the domain entity should never reach the serialiser.
Serialisation-Level Controls: When and When Not to Use Them
System.Text.Json and Newtonsoft.Json both offer mechanisms for controlling what gets serialised:
[JsonIgnore]โ skips a property entirely[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]โ skips nulls onlyCustom converters โ intercept serialisation to mask or redact values
[JsonPropertyName]โ rename or alias a field in the output
These tools have their place, but they should not be your primary defence against data exposure. They are appropriate for cosmetic output control (e.g., omitting nulls in large response objects) and for logging-safe serialisation (e.g., a MaskedStringConverter that replaces card numbers with ****1234). They are not appropriate as the mechanism that prevents a password hash from appearing in an API response. That responsibility belongs at the DTO boundary โ before the data ever reaches the serialiser.
Where serialisation controls genuinely help:
Masking PII in structured logs (
ILoggersinks see your objects โ mask at the model level, not the log sink)Suppressing null fields to reduce response payload size
Preventing accidental serialisation of navigation properties that EF Core may have loaded
Handling Error Responses: Never Expose Exception Details
One of the most common sensitive data leaks in ASP.NET Core APIs happens not in the happy path but in error handling. When an unhandled exception propagates through the request pipeline without proper global handling, the default behaviour in development mode is to return the full exception message, stack trace, and sometimes inner exception details. Teams that accidentally deploy with ASPNETCORE_ENVIRONMENT=Development โ or that have a misconfigured exception middleware โ expose this to external callers.
The rule is simple: production APIs must never return exception.Message or stack traces to clients. Error responses should be based on RFC 7807 Problem Details โ a structured format that communicates the error type and a human-readable title without leaking internals:
{
"type": "https://tools.ietf.org/html/rfc7807",
"title": "An unexpected error occurred.",
"status": 500,
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
The traceId allows your team to correlate the error in your observability system without surfacing any details to the client. The IExceptionHandler interface in .NET 8+ makes it straightforward to centralise this logic โ mapping known exception types to appropriate HTTP status codes and Problem Details shapes, while treating all unrecognised exceptions as opaque 500 errors.
For teams working with an older pipeline, a custom exception-handling middleware with the same mapping logic achieves the same outcome.
The mass assignment guide published earlier on this blog โ Preventing Mass Assignment Vulnerabilities in ASP.NET Core APIs โ covers a closely related data leak vector that often travels alongside exposure risks.
Logging Hygiene: Sensitive Data in Observability Pipelines
Your API probably logs request and response data for observability โ distributed traces, structured log entries, or both. This is valuable, but it creates a secondary exposure surface. PII and sensitive field values that never appear in the HTTP response can still end up in your Sentry events, your OpenTelemetry traces, or your Serilog file sinks if you log objects carelessly.
Key principles for logging hygiene in ASP.NET Core:
Log correlators, not values. Log the user ID, not the user's email address. Log the transaction ID, not the transaction amount. This limits the blast radius if your log storage is ever accessed by an unauthorised party.
Use Serilog destructuring policies. Serilog's IDestructuringPolicy lets you intercept how objects are logged and redact or mask specific properties before they hit any sink. This is the correct place to suppress sensitive fields from log output โ not the domain model.
Never log request bodies wholesale in production. Middleware that logs the full request body (a common debugging approach) will capture authentication headers, credit card numbers, or passwords if any caller submits them. If you need request logging, log only the correlation ID and a sanitised summary.
ASP.NET Core's built-in UseSerilogRequestLogging (from the Serilog.AspNetCore package) logs HTTP request details using a single structured log entry per request โ covering method, path, status code, and duration โ without logging headers or bodies by default. This is the right baseline.
Authorization Boundaries and Field-Level Visibility
Sometimes the right response shape is not a function of the endpoint alone but of the caller's role. An admin endpoint that returns a user record should include fields that the user's own self-service endpoint should not.
ASP.NET Core's IAuthorizationService supports resource-based authorization decisions inside a handler, but for field-level visibility โ deciding which fields appear in the response based on the caller's claims โ the cleanest pattern is role-aware DTO selection rather than runtime property nulling.
Rather than taking a single large response object and conditionally nulling out fields based on the caller's role (which is error-prone and hard to test), map to different DTOs at the controller level:
if (User.IsInRole("Admin"))
return Ok(mapper.Map<UserAdminResponse>(user));
return Ok(mapper.Map<UserSummaryResponse>(user));
This approach means that field visibility is enforced at compile time for each DTO, not at runtime by conditional logic that could easily be missed in a code review. It also makes both response shapes independently testable.
For related reading on authorization in ASP.NET Core APIs, the post on Preventing Broken Object Level Authorization (BOLA) covers the access control side of the same problem.
Defence-in-Depth Checklist
Before marking a security review complete for any ASP.NET Core API endpoint:
No entity types as return types โ every endpoint returns a dedicated response DTO
No
[JsonIgnore]as a security control โ it is permitted as a cosmetic tool onlyError responses use Problem Details โ no
exception.Messageor stack traces visible to clientsLog objects use destructuring policies โ PII is redacted at the sink, not suppressed by forgetting to log it
PII fields in DTOs are reviewed for minimum necessary data โ return partial data (last 4 of card, masked email) where the full value is not required by the caller's use case
Role-aware DTO selection is explicit โ admin and public response shapes are separate types, not the same type with nulled fields
EF Core queries use
AsNoTrackingandSelectprojections โ avoid loading fields that are not needed in the responseHTTP response headers do not leak server/framework version โ
Server,X-Powered-By, andX-AspNet-Versionheaders should be suppressed in production
What Penetration Testers Look For
Understanding what a security assessor checks helps teams prioritise where to invest.
In practice, pen testers will proxy all API traffic and examine every response for fields that should not be there. Common findings in .NET APIs:
Integer database IDs in public endpoints (enumerable โ enables IDOR attacks)
passwordHash,securityStamp,concurrencyStampfrom IdentityUserInternal status codes, feature flags, or A/B test assignments
createdAt/updatedAtfor objects where modification history is sensitiveFull email addresses where only masked versions are needed
Exception messages in 500 responses confirming internal infrastructure details (database type, ORM name, table names)
Each of these is a finding that a DTO-first response design eliminates before testing even begins.
โ Prefer a one-time tip? Buy us a coffee โ every bit helps keep the content coming!
FAQ
What is sensitive data exposure in ASP.NET Core Web APIs?
Sensitive data exposure occurs when an ASP.NET Core API returns more information in its HTTP response than the caller is authorised to see. This includes internal fields such as password hashes, audit metadata, role flags, database IDs, or PII. The OWASP API Security Top 10 classifies this as API3: Excessive Data Exposure. The typical cause is returning domain entities or EF Core entities directly from controllers instead of purpose-built response DTOs.
How do I prevent sensitive data exposure in an ASP.NET Core API response?
The most reliable prevention strategy is to map all endpoint responses to dedicated response DTOs that contain only the fields the caller needs. Never return EF Core entity objects directly. Avoid relying on [JsonIgnore] as a security boundary โ it is fragile and cannot express role-based visibility. Use Select projections in EF Core queries to avoid loading fields that will not appear in the response, and centralise error handling with Problem Details so that exception messages never reach the client.
Should I use [JsonIgnore] to hide sensitive fields in ASP.NET Core?
[JsonIgnore] is appropriate for cosmetic serialisation control โ suppressing nulls, excluding non-public computed properties, or preventing navigation property loops. It should not be used as a security control to hide sensitive data, because it applies unconditionally to all callers, cannot express role-based logic, and does not prevent the field from being loaded by EF Core or appearing in logs. Use dedicated response DTOs instead.
How does the OWASP API Security Top 10 relate to sensitive data exposure in .NET?
OWASP API3: Excessive Data Exposure maps directly to the pattern of returning too many fields in an API response. The 2023 OWASP API Security Top 10 updated this category to "Broken Object Property Level Authorization," reflecting that field-level exposure is an authorization problem โ callers receive data they are not authorised to see. In ASP.NET Core, this is addressed by combining proper response DTO design with role-aware DTO selection and IAuthorizationService for resource-level access control.
What is the difference between sensitive data exposure and a data breach?
A data breach typically involves an external attacker exploiting a vulnerability โ SQL injection, SSRF, stolen credentials โ to access data they have no path to through normal API flows. Sensitive data exposure means the API is returning the data voluntarily, as part of its normal response, to callers who are not supposed to see it. No exploit is required. This distinction matters because prevention strategies are different: data breaches require hardening attack surfaces, while sensitive data exposure requires hardening your API's output contracts.
How do I prevent sensitive data from appearing in logs in ASP.NET Core?
Use Serilog's IDestructuringPolicy to intercept how objects are logged and mask or redact sensitive properties before they reach any sink. Avoid logging request bodies wholesale in production. Prefer logging correlators (user ID, transaction ID) over values (email, amount). The UseSerilogRequestLogging middleware provides a good baseline โ it logs request metadata without headers or body content by default.
Is there a way to return different fields for different roles in ASP.NET Core?
Yes โ the recommended approach is role-aware DTO selection. Rather than taking one large response object and conditionally nulling fields based on the caller's role (which is fragile and hard to review), define separate response DTOs for each role level and map to the appropriate one at the controller level using User.IsInRole() or an authorization policy check. This makes each response shape independently testable and enforces field visibility at compile time rather than runtime.
Does using EF Core's AsNoTracking help with sensitive data exposure?
AsNoTracking improves performance on read-only queries by disabling change tracking, but it does not by itself prevent sensitive data exposure โ the entity is still loaded with all its properties. The important companion pattern is using Select projections to query only the fields you need, combined with mapping to a response DTO. Together, AsNoTracking + Select ensures that sensitive fields are neither loaded by the database query nor present in the object graph that reaches your serialiser.






