Skip to main content

On This Page

Securing .NET APIs: Preventing Information Disclosure via Exception Handling

3 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

What Your .NET Exceptions Are Telling Attackers (And How to Stop It)

Default ASP.NET Core development settings can expose internal file paths, database hostnames, and even cleartext credentials to any API caller. This information disclosure provides attackers with a detailed roadmap of your internal architecture and database schema.

Why This Matters

In technical reality, a single unhandled exception in production can bypass security boundaries by revealing project layouts and SQL fragments. While developers rely on detailed logs for debugging, exposing these same details to clients creates a massive vulnerability, necessitating a strict separation between internal observability and external error responses to prevent database schema and credential leaks.

Key Insights

  • Stack traces reveal internal folder structures and library versions, which attackers use to identify known vulnerabilities in specific dependencies.
  • RFC 7807 Problem Details, supported in ASP.NET Core 7+, provides a standardized way to return errors without exposing sensitive internal state.
  • The IExceptionHandler interface in .NET 8 allows for a clean chain of responsibility when handling domain-specific versus global exceptions.
  • String interpolation in logging can accidentally leak password hashes or PII if the object’s ToString() method is not strictly controlled.
  • Timing attacks can reveal valid usernames even with identical error messages if password hash comparisons are skipped for non-existent users.

Working Examples

Implementing the .NET 8 IExceptionHandler for secure, global error handling.

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) => _logger = logger;

    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        var correlationId = httpContext.TraceIdentifier;
        _logger.LogError(exception, "Unhandled exception. CorrelationId: {CorrelationId}", correlationId);

        var (statusCode, title) = exception switch
        {
            UnauthorizedAccessException => (401, "Unauthorised."),
            KeyNotFoundException => (404, "Resource not found."),
            _ => (500, "An unexpected error occurred.")
        };

        httpContext.Response.StatusCode = statusCode;
        await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Title = title,
            Status = statusCode,
            Extensions = { ["traceId"] = correlationId }
        }, cancellationToken);

        return true;
    }
}

Removing the Server header from Kestrel to reduce technology fingerprinting.

builder.WebHost.ConfigureKestrel(options =>
{
    options.AddServerHeader = false;
});

Practical Applications

  • Use Case: Implement GlobalExceptionHandler in .NET 8 to map internal exceptions to generic 401/404/500 status codes. Pitfall: Returning ex.Message directly to the client, which often contains internal SQL fragments or sensitive configuration values.
  • Use Case: Use constant-time dummy password verification for non-existent users to prevent timing-based username enumeration. Pitfall: Deploying with ASPNETCORE_ENVIRONMENT set to Development, which enables verbose stack traces in production.
  • Use Case: Configure Serilog destructuring policies to automatically redact ‘Password’ fields from log objects. Pitfall: Using string interpolation in logs which serializes entire objects, potentially leaking PII into plain-text log stores.

References:

Continue reading

Next article

A Complete History of AWS: Launch Dates from SQS to 2026

Related Content