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