Skip to content

CWE-201: Insertion of Sensitive Information Into Sent Data

Overview

CWE-201 occurs when an application includes confidential data - passwords, tokens, internal paths, stack traces, or PII - in data it deliberately sends: HTTP responses, API payloads, serialized objects, or outbound requests to third parties. CWE-209 is specifically about error-message text; CWE-201 covers any sent data channel, most commonly a full database entity serialized straight into an API response.

Relationship to Other CWEs

OWASP Classification

A01:2025 - Broken Access Control

Risk

Medium: A response built from an internal object carries whatever that object holds - password hashes, session tokens, API keys, PII, and internal detail such as file paths, versions and database structure. A token or key that reaches an unauthorized caller is usable as it stands, which is the account-takeover path; the rest is either a privacy breach in its own right or reconnaissance for a more targeted attack.

Remediation Steps

Core Principle: Build every outbound payload from an explicit allowlist of safe fields; never serialize an internal object directly and hope nothing sensitive is in it.

Trace the Data Path

  • Source: database entities, exception objects, environment/configuration, internal service responses
  • Sink: HTTP API responses, error responses, logs treated as sent data, outbound requests to third parties, client-side JavaScript bundles
  • Data Flow / Missing Controls: an internal object is serialized directly (return jsonify(user), return user_object) with no intermediate step that selects only the fields meant to be public

Use an Allowlist Response Model (Primary Defense)

  • Define a response contract, not a redaction list: create an explicit DTO/view-model type per endpoint that names only the safe fields; never serialize the underlying entity/model
  • Never return raw exceptions to a client: catch at the application boundary, log full detail server-side with a correlation ID, return a generic message
  • Prevent enumeration: return the identical response for "not found" and "not authorized" so response shape and status code cannot be used to probe for valid resources or usernames

Check What the Framework Sends Where Your Code Sends Nothing

An allowlisted response model governs the responses your handlers write. It does not govern the ones the framework writes on your behalf, and those are the copies that never appear in a diff:

  • The validation-rejection body. Several frameworks include the rejected value in the error they generate, so a too-short password is returned to the caller and written to every access log on the way. Replace the default body with one that names the failing field and not its contents.
  • The 404, 405 and 415 paths. These are produced before your handler runs. Confirm what they return, and confirm a catch-all error handler has not taken them over - a handler registered for the base exception type will, in several frameworks, answer 500 to all of them.
  • Debug and diagnostic endpoints. Framework debug pages, health endpoints reporting detail, and configuration or metrics endpoints each serialize independently of your response models.

Apply Defense in Depth

  • Sanitize logs and debug output: never write passwords, tokens, full card numbers, or CVVs to logs, even at debug level; redact before writing, not after
  • Redact the exception as well as the message: log redactors are usually attached to the message, while the framework renders an exception and its stack trace through a separate path - so a driver or validation exception quoting the offending value is written unfiltered by a control that looks correct
  • Disable verbose/debug modes in production: stack traces, framework debug pages, and admin/actuator endpoints must not be reachable by unauthenticated or unauthorized callers
  • Keep the subsystem out of generic errors: a message or code naming the failing component (DB_ERROR, "A database error occurred") reads as sanitized while still confirming what sits behind the endpoint. Ask whether the string describes the caller's situation or your architecture; log the second kind with an opaque error ID standing in for it
  • Review third-party integrations: outbound webhooks, logging-as-a-service, and analytics calls are still "sent data" - apply the same allowlist to anything forwarded off-system

Test with Malicious and Boundary Inputs

  • Request a resource you are not authorized to see and confirm the response contains no data at all, not partially redacted data
  • Trigger a server error and confirm the response has no stack trace, file path, or internal exception text
  • Diff the full internal entity against the actual response shape whenever a new field is added to the entity, to catch silent over-exposure
  • Re-scan with the security tool that reported the finding to confirm it is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
record = database.get(id)
send_response(record)
// record contains password_hash, internal_id, and other users' data
// Attack: any caller who can reach this endpoint receives every field

Why this is vulnerable: nobody decided to send password_hash. The line says "send the record", and a serializer's default is to emit every field it can read, so what leaves the process is chosen by the shape of the database row rather than by anything written here. Two things follow that the code does not show: there is no statement of intent for a reviewer to check, and the exposure renews itself - the next column added to the table ships to clients the moment it is mapped, with no change to this function at all.

Nothing fails when it happens. The response is a success, tests that assert on the fields they care about still pass, and no log records that more was sent than was asked for. The defect is visible only in the response body, which is why it survives in code that is otherwise carefully reviewed - it is found by reading a response, not by reading the source.

Secure Patterns

// SECURE - pseudo-code
record = database.get(id)
if not authorized(actor, record):
    send_response(NOT_FOUND, generic=true)
else:
    dto = build_from_allowlist(record, PUBLIC_FIELDS)
    send_response(dto)

Why this works: The response is constructed from a fixed set of named fields rather than the internal record itself, so a field that is never added to PUBLIC_FIELDS cannot leak even if it is added to the underlying entity later. Checking authorization before building the response, and returning the same generic result for "not found" and "not authorized", removes the response itself as a channel for enumeration.

Common Pitfalls

  • Redacting known-sensitive fields instead of allowlisting safe ones: deleting password_hash and ssn from a serialized object before sending it still exposes any field nobody thought to redact, and a newly added entity field ships exposed by default.
  • Generic error message in the body, but the stack trace still logged to a client-visible channel: browser dev consoles, client-side error trackers, or a debug header can still carry the same internal detail the response body was cleaned of.
  • Masking only part of a sensitive value: returning the last four digits of a card number or a truncated token feels safe, but the remaining fragment can still narrow a brute-force search or match against another leaked dataset.
  • Fixing the primary API response but not the webhook/log/analytics copy of the same data: the same record often gets sent to a second, less-reviewed channel (audit log, monitoring event, third-party integration) that still serializes the full object.

Language-Specific Guidance

  • Java - Spring Boot, Jakarta EE with DTOs, exception handlers, and Jackson configuration
  • JavaScript/Node.js - Express, NestJS, React, Next.js with field selection and environment variable management
  • Python - Flask, Django, FastAPI with DTOs, error handling, and secure logging

Additional Resources