Skip to content

CWE-209: Generation of Error Message Containing Sensitive Information

Overview

CWE-209 occurs when detailed error messages are exposed to users, revealing sensitive information about the application's internal structure, configuration, or data.

Relationship to Other CWEs

  • CWE-209 (this page) - an error message shown to a user carries detail about the application's internal structure, configuration or data.
  • CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor) - a parent of CWE-209, along with CWE-755 (Improper Handling of Exceptional Conditions), which has no page here yet. CWE-209 is the specific case where the exposure channel is an error message.
  • CWE-201 (Insertion of Sensitive Information Into Sent Data) - overlaps with CWE-209 when the error message is the sent data. Use CWE-201 when the finding is about a broader response payload rather than error text.
  • CWE-210, CWE-211 and CWE-550 - children of CWE-209 covering the self-generated, externally-generated and server-generated error message variants. None has a page here yet; this page covers the general case regardless of which layer generated the message.

OWASP Classification

A10:2025 - Mishandling of Exceptional Conditions

Risk

Medium: A leaked stack trace, file path or database error tells an attacker which frameworks, libraries and schema are behind the endpoint, which narrows the search for an exploitable weakness. Error text that differs between "no such user" and "wrong password" also confirms which accounts exist.

Remediation Steps

Core Principle: Never expose internal error details to clients; error responses must be constructed from a fixed, server-controlled contract that is independent of internal exception state.

Locate the error message information leak

  • Find where detailed error messages reach users
  • Identify what is leaking: stack traces, file paths, database errors, internal configuration, SQL queries, or user enumeration
  • Trace the error flow: where exceptions are caught and how the resulting messages are returned to users
  • Check every error surface: 404 and 500 pages, exception handlers, and API error responses

Show generic error messages to users (Primary Defense)

  • Display only generic messages in production: "An error occurred", "Invalid credentials", "Resource not found"
  • Do not send stack traces, file paths, SQL errors, or other internal system information to users
  • Return a unique error ID for support reference instead of a description, so the user-facing message can be correlated with the server log
  • Use the same message for related failures, such as "user doesn't exist" and "wrong password", so the response cannot be used for user enumeration

Log detailed errors securely on the server side

  • Log the diagnostic details server-side: stack trace, exception class, sanitized request context, user identifier, and timestamp
  • Exclude or redact credentials, tokens, cookies, request bodies, credit card numbers, and PII from error logs
  • Log the same error ID that was returned to the user so support can find the matching entry
  • Restrict access to detailed error logs to the operations and development staff who need it
  • Use structured logging with separate fields for timestamp, level, message, and context so the logs can be searched and analyzed

Validate and sanitize all error output

  • Keep user input out of error messages; if it is unavoidable, sanitize and truncate it first
  • Redact credentials, tokens, and PII from any error output
  • Disable debug mode in production: verbose error pages, stack trace display, and debug flags
  • Configure custom 404 and 500 pages that do not reveal the framework or its version

Monitor and audit error reporting

  • Review error logs regularly for leaked secrets: search for passwords, tokens, and internal paths
  • Alert on repeated or unusual error patterns, which may indicate attack attempts or an application fault
  • Watch error rates and types for anomalies such as a spike in 500 responses
  • Review the framework's error handling configuration to confirm debug modes and stack trace display are off

Test the error handling fix

  • Trigger a range of errors with invalid inputs: malformed data, missing parameters, wrong types
  • Confirm users see only generic messages, with no stack traces, paths, or technical details
  • Confirm detailed errors are logged server-side with tracking IDs
  • Confirm the 404 and 500 pages display generic messages
  • Check that API error responses are well-formed and do not expose internals
  • Re-scan with the security scanner to confirm the issue is resolved

Common Vulnerable Patterns

Exposing Stack Traces and Exception Details to Users

// VULNERABLE - pseudo-code
try {
    perform_operation()
} catch (error) {
    // WRONG: exposes stack trace, database errors, file paths
    return_to_user(error.message)
    return_to_user(error.stack_trace)
}

Why this is vulnerable: Raw exception messages and stack traces reveal file paths, database schema, SQL queries, library versions, and code structure. An attacker uses those details to map the application architecture and to identify dependencies with known vulnerabilities.

Secure Patterns

Generic Error Messages with Server-Side Logging

// SECURE - pseudo-code
try {
    perform_operation()
} catch (error) {
    // Log sanitized diagnostic details server-side
    error_id = generate_unique_id()
    log_error(error_id, error.message, error.stack_trace, sanitized_request_details)

    // Return only generic error with tracking ID
    return_to_user({
        error: "An error occurred",
        error_id: error_id,
        message: "Please contact support with this error ID"
    })
}

Why this works: The user-facing message is separated from the internal log. "An error occurred" names no database schema, file path or library version, so an attacker who triggers errors deliberately to map the application gets nothing back. The unique error_id ties what the user sees to the server-side log entry, so support can troubleshoot without exposing any detail to the client. The full exception context, including the stack trace, request parameters and user session, is captured where only authorized staff can read it.

Common Pitfalls

  • Generic message in the response body, but the framework's default error page still enabled: an application-level try/catch that returns a clean JSON error can still be bypassed by an unhandled exception that reaches the framework's own debug or stack-trace page, such as Flask debug mode, ASP.NET's default developer exception page, or Spring Boot's whitelabel error page, if that page was never explicitly disabled for production.
  • Sanitizing the message but not the exception type or error code: stripping the text of a database exception while still returning its class name (PSQLException, SqlException) or a vendor-specific error code still tells an attacker which database engine and driver are in use. The coarse version of this reads as already-sanitized and is the one that survives review: a hand-written "A database error occurred", or an application error code of DB_ERROR, names the failing subsystem without naming the product. It still confirms there is a database behind the endpoint, still separates "the query broke" from "the template broke" for someone deciding what to probe next, and still gives a legitimate user nothing they can act on. The test to apply to any client-visible message or code is whether it describes the caller's situation or your architecture: Resource not found and Authentication failed are facts about the request and are safe to state plainly; anything a reader could use to sketch your stack belongs in the log line, with an opaque error ID standing in for it in the response.
  • Redacting only the first error, not a chained/wrapped exception's cause: logging or returning error.message looks generic, but if that message was built by concatenating a lower-level exception's own message ("Payment failed: " + causeException.getMessage()), the sensitive detail is still embedded in the string that ships to the user.
  • Different response timing or status code between error types: even with identical error text, a measurably slower response for "invalid password" versus "user not found," or a 500 for one failure mode and a 401 for another, still leaks which case occurred.
  • The validation framework's default error response, which nobody wrote and so nobody reviews: most request-validation layers answer a rejected field by returning the internal field name, the constraint it failed, and the value that was submitted. FastAPI/Pydantic's default 422 body includes an input key holding the rejected value; Spring's ObjectError.toString() renders rejected value [...]; express-validator's errors.array() carries a value per entry. On a registration or password-change endpoint that means the submitted password is echoed back to the caller and written into every access log and error tracker on the way. It is not in the diff, because it is the framework's behaviour rather than a line of application code. Build the response from the field name plus a message you wrote, and log the field paths rather than the values.
  • A catch-all handler that also catches the framework's own client errors: registering a handler for the language's base exception type to sanitize unexpected failures can capture routing errors too, turning a 404, 405, or 413 into a 500. Nothing leaks, so a security re-scan passes, but every client that distinguishes "you sent the wrong method" from "the server broke" now cannot, and the error log fills with 500s that are not. Register the framework's HTTP-exception type ahead of the catch-all and pass its status through.

Language-Specific Guidance

  • Go - Custom error types, panic recovery, generic error responses
  • Java - Spring @ExceptionHandler, ResponseEntity, custom error pages
  • JavaScript/Node.js - Express error middleware, error ID generation, production config
  • Python - Flask errorhandler decorator, generic exceptions, logging with UUID

Additional Resources