Skip to content

CWE-201: Insertion of Sensitive Information Into Sent Data

Overview

Insertion of sensitive information into sent data occurs when applications include confidential data (passwords, tokens, internal paths, stack traces, PII) in HTTP responses, error messages, logs, or API responses that are transmitted to users or external systems, enabling information disclosure.

OWASP Classification

A01:2025 - Broken Access Control

Risk

Medium: Exposing sensitive information in responses reveals passwords, session tokens, internal system details (paths, versions), PII, business logic, database structure, and API keys to unauthorized parties. This enables further attacks, account takeover, and privacy violations.

Remediation Steps

Core principle: Prevent leaking sensitive data in responses/metadata; scrub secrets and avoid embedding sensitive values in outbound data.

Locate Sensitive Information in Sent Data

When reviewing security scan results:

  • Identify what sensitive data is exposed: Passwords, tokens, session IDs, API keys, PII, internal paths, stack traces, database structure, business logic
  • Find where data is sent: HTTP responses, error messages, API responses, logs, debug output, client-side JavaScript
  • Check serialization points: Where objects are converted to JSON/XML for transmission
  • Review error handlers: Exception handlers that may expose stack traces or detailed errors
  • Trace data flow: From database/internal systems to user-facing responses

Common exposure points:

# Returning full user object with password
return jsonify(user)  # User model has password_hash field!

# Stack trace in error response
try:
    risky_operation()
except Exception as e:
    return str(e), 500  # Exposes internal paths!

# Logging sensitive data
logger.info(f'Login: {username} / {password}')  # Password in logs!

# Debug info in production
if error:
    return {'error': error, 'traceback': traceback.format_exc()}

Filter Sensitive Data from Responses (Primary Defense)

from flask import Flask, jsonify
from dataclasses import dataclass

# VULNERABLE - returning full user object
@app.route('/api/user/<int:user_id>')
def get_user_bad(user_id):
    user = db.query(User).get(user_id)

    # Attack: User model contains password_hash, ssn, internal_id
    return jsonify(user)  # Exposes ALL fields including sensitive ones!

# SECURE - use Data Transfer Object (DTO)
@dataclass
class UserDTO:
    """Public user representation - only safe fields."""
    id: int
    username: str
    email: str
    created_at: str
    # NO password_hash, ssn, or internal fields

@app.route('/api/user/<int:user_id>')
def get_user_safe(user_id):
    user = db.query(User).get(user_id)

    if not user:
        return jsonify({'error': 'Not found'}), 404

    # Create DTO with only public fields
    user_dto = UserDTO(
        id=user.id,
        username=user.username,
        email=user.email,
        created_at=user.created_at.isoformat()
        # Explicitly omit: password_hash, ssn, internal_id
    )

    return jsonify(user_dto)

# Alternative: explicit field selection
@app.route('/api/user/<int:user_id>')
def get_user_safe_v2(user_id):
    user = db.query(User).get(user_id)

    if not user:
        return jsonify({'error': 'Not found'}), 404

    # Allowlist specific fields
    return jsonify({
        'id': user.id,
        'username': user.username,
        'email': user.email,
        'created_at': user.created_at.isoformat()
    })

Why this works: DTOs create an explicit contract of what fields are safe to expose. Never serialize entire database entities - they often contain internal fields, passwords, or sensitive data not meant for users.

Use Generic Error Messages (Prevent Information Leakage)

import logging
import uuid
from flask import Flask, jsonify

app = Flask(__name__)
logger = logging.getLogger(__name__)

# VULNERABLE - exposing detailed errors
@app.route('/api/data')
def get_data_bad():
    try:
        data = db.execute('SELECT * FROM sensitive_table')
        return jsonify(data)
    except Exception as e:
        # Attack: exposes SQL error, table names, file paths
        return jsonify({
            'error': str(e),  # "Table 'sensitive_table' doesn't exist"
            'traceback': traceback.format_exc()  # Full stack trace!
        }), 500

# SECURE - generic errors with server-side logging
@app.route('/api/data')
def get_data_safe():
    error_id = str(uuid.uuid4())

    try:
        data = db.execute('SELECT * FROM sensitive_table')
        return jsonify(data)
    except Exception as e:
        # Log full details server-side with error ID
        logger.error(
            f'Error ID {error_id}: {str(e)}',
            exc_info=True,
            extra={
                'error_id': error_id,
                'endpoint': '/api/data',
                'user': get_current_user_id()
            }
        )

        # Return only generic error to user
        return jsonify({
            'error': 'An error occurred processing your request',
            'error_id': error_id  # For support tracking only
        }), 500

# Custom error handler
@app.errorhandler(Exception)
def handle_exception(e):
    error_id = str(uuid.uuid4())

    # Log detailed error server-side
    logger.error(f'Error ID {error_id}', exc_info=True)

    # Generic message to user
    if app.debug:
        # Only in development
        return jsonify({'error': str(e), 'error_id': error_id}), 500
    else:
        # Production: no details
        return jsonify({
            'error': 'Internal server error',
            'error_id': error_id
        }), 500

Why this works: Generic error messages prevent attackers from learning about internal system structure (table names, file paths, technologies). The error ID links user reports to detailed server logs without exposing sensitive information to the client.

Prevent User Enumeration

import time
import secrets

# VULNERABLE - different errors reveal user existence
@app.route('/login', methods=['POST'])
def login_bad():
    username = request.form['username']
    password = request.form['password']

    user = db.query(User).filter_by(username=username).first()

    if not user:
        # Attack: reveals user doesn't exist
        return jsonify({'error': 'User not found'}), 401

    if not check_password(user, password):
        # Attack: reveals user exists, wrong password
        return jsonify({'error': 'Invalid password'}), 401

    return jsonify({'token': create_token(user)})

# SECURE - identical error for all authentication failures
@app.route('/login', methods=['POST'])
def login_safe():
    username = request.form['username']
    password = request.form['password']

    # Always perform same operations (constant time)
    user = db.query(User).filter_by(username=username).first()

    # Perform hash comparison even if user doesn't exist
    if user:
        password_valid = check_password(user, password)
    else:
        # Dummy comparison to prevent timing attacks
        dummy_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
        password_valid = False

    # Add random delay to prevent timing attacks
    time.sleep(secrets.SystemRandom().uniform(0.1, 0.3))

    if user and password_valid:
        logger.info(f'Successful login: {username}')
        return jsonify({'token': create_token(user)})
    else:
        # Same error message for all failures
        logger.warning(f'Failed login attempt: {username}')
        return jsonify({'error': 'Invalid credentials'}), 401

Why this works: Identical error messages and timing for all authentication failures prevent attackers from determining which usernames exist in the system. The dummy hash computation and random delay prevent timing-based user enumeration attacks.

Sanitize Logs and Debug Output

import logging
import re

class SensitiveDataFilter(logging.Filter):
    """Filter to redact sensitive data from logs."""

    SENSITIVE_PATTERNS = [
        (r'password["\s:=]+([^\s,}]+)', r'password=<REDACTED>'),
        (r'token["\s:=]+([^\s,}]+)', r'token=<REDACTED>'),
        (r'api[_-]?key["\s:=]+([^\s,}]+)', r'api_key=<REDACTED>'),
        (r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b', r'<CREDIT_CARD>'),
        (r'\b\d{3}-\d{2}-\d{4}\b', r'<SSN>')
    ]

    def filter(self, record):
        message = record.getMessage()
        for pattern, replacement in self.SENSITIVE_PATTERNS:
            message = re.sub(pattern, replacement, message, flags=re.IGNORECASE)
        record.msg = message
        return True

# Configure logger with filter
logger = logging.getLogger(__name__)
logger.addFilter(SensitiveDataFilter())

# VULNERABLE - logging sensitive data
@app.route('/payment', methods=['POST'])
def process_payment_bad():
    card_number = request.form['card_number']
    cvv = request.form['cvv']

    # BAD: logs credit card!
    logger.info(f'Processing payment with card: {card_number}')

    # BAD: debug print in production
    print(f'CVV: {cvv}')

    return jsonify({'status': 'success'})

# SECURE - never log sensitive data
@app.route('/payment', methods=['POST'])
def process_payment_safe():
    card_number = request.form['card_number']
    cvv = request.form['cvv']

    # Safe: log only last 4 digits
    last_4 = card_number[-4:] if len(card_number) >= 4 else 'XXXX'
    logger.info(f'Processing payment ending in {last_4}')

    # NEVER log CVV, full card number, passwords, tokens

    result = payment_processor.charge(card_number, cvv)

    return jsonify({'status': 'success'})

# Disable debug mode in production
import os
app.config['DEBUG'] = os.getenv('FLASK_ENV') == 'development'

Why this works: Logging filters automatically redact sensitive patterns before they're written to logs. Logging only masked/partial data (last 4 digits) provides audit trails without exposing full credentials. Disabling debug mode prevents verbose error output in production.

Test for Information Disclosure

import pytest
from unittest.mock import patch

def test_user_endpoint_no_password():
    """Verify password not exposed in user API."""
    response = client.get('/api/user/1')
    data = response.get_json()

    # Ensure response doesn't contain sensitive fields
    assert 'password' not in data
    assert 'password_hash' not in data
    assert 'ssn' not in data
    assert 'internal_id' not in data

    # Verify expected public fields present
    assert 'username' in data
    assert 'email' in data
    print("✓ User endpoint doesn't expose sensitive fields\n")

def test_error_message_generic():
    """Verify errors don't expose stack traces."""
    with patch('app.db.execute', side_effect=Exception('Database error')):
        response = client.get('/api/data')
        data = response.get_json()

        # Should have generic error
        assert 'error' in data
        assert data['error'] == 'An error occurred processing your request'

        # Should NOT have technical details
        assert 'Database error' not in str(data)
        assert 'traceback' not in data
        assert 'stack' not in data

        # Should have error ID for tracking
        assert 'error_id' in data

    print("✓ Error messages are generic\n")

def test_user_enumeration_prevention():
    """Verify same error for invalid user and wrong password."""
    # Non-existent user
    resp1 = client.post('/login', data={
        'username': 'nonexistent',
        'password': 'anything'
    })

    # Valid user, wrong password
    resp2 = client.post('/login', data={
        'username': 'validuser',
        'password': 'wrongpassword'
    })

    # Both should have identical error message
    assert resp1.status_code == resp2.status_code == 401
    assert resp1.get_json()['error'] == resp2.get_json()['error']
    assert resp1.get_json()['error'] == 'Invalid credentials'

    print("✓ User enumeration prevented\n")

def test_logs_no_sensitive_data(caplog):
    """Verify logs don't contain sensitive data."""
    with caplog.at_level(logging.INFO):
        client.post('/login', data={
            'username': 'testuser',
            'password': 'SecretPassword123!'
        })

    # Check no password in logs
    for record in caplog.records:
        assert 'SecretPassword123!' not in record.message
        assert 'password' not in record.message.lower() or '<REDACTED>' in record.message

    print("✓ Logs don't contain passwords\n")

if __name__ == '__main__':
    test_user_endpoint_no_password()
    test_error_message_generic()
    test_user_enumeration_prevention()
    test_logs_no_sensitive_data()
    print("All information disclosure tests passed!\n")

Why this works: Automated tests verify that sensitive data isn't exposed in responses, errors, or logs. These tests catch regressions when new fields are added or error handling changes, ensuring information disclosure protections remain effective.

Common Vulnerable Patterns

  • Returning passwords/tokens in JSON responses
  • Stack traces in error responses
  • Internal file paths in error messages
  • Full user objects (including password hashes)
  • Detailed SQL errors to users

Language-Specific Code Examples

For detailed, production-ready code examples in your programming language, see:

  • 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

Each language-specific guide includes:

  • Complete vulnerable and secure code patterns
  • Framework-specific implementations
  • Testing examples
  • Common pitfalls

Generic Secure Patterns

Use Data Transfer Objects (DTOs)

Create explicit response objects that contain only safe fields:

  • Define a separate class/object for API responses
  • Include only public, non-sensitive fields
  • Never return database entities/models directly
  • Use explicit field mapping from entity to DTO

Generic Error Messages with Server-Side Logging

Handle errors with two-tier approach:

  • Log full error details (stack trace, context) server-side only
  • Return generic error messages to users
  • Use error codes for tracking without exposing details
  • Never send stack traces, SQL errors, or file paths to clients

Prevent User Enumeration

Use consistent error messages for authentication:

  • Return same error for "user not found" and "wrong password"
  • No indication of which usernames/emails exist in system
  • Consider timing attack prevention with constant-time operations
  • Log detailed authentication failures server-side only

Filter Sensitive Data from Logs

Implement logging filters and sanitization:

  • Never log passwords, tokens, credit cards, API keys
  • Redact or mask sensitive fields before logging
  • Use custom logging filters/formatters
  • Override toString() methods to exclude sensitive fields
  • Configure logging frameworks to filter patterns

Secure Configuration Management

Protect secrets and configuration:

  • Never hardcode credentials in source code
  • Use environment variables for secrets
  • Disable debug mode in production
  • Restrict access to configuration endpoints
  • Don't expose environment variables to client-side code

Language-Specific Guidance

  • Java - DTOs with @JsonView, @JsonIgnore, custom error handlers
  • JavaScript/Node.js - Field selection, custom error middleware, environment variables
  • Python - Dataclasses for DTOs, logging filters, Flask error handlers

Security Checklist

  • API responses use DTOs, not full database entities
  • No passwords, tokens, API keys in responses
  • Error messages are generic (no stack traces in production)
  • User enumeration prevented (same error for all auth failures)
  • Logs filtered for sensitive data (passwords, cards, SSNs)
  • Debug mode disabled in production
  • Tests verify no sensitive data in responses or logs

Additional Resources