CWE-201: Insertion of Sensitive Information Into Sent Data - Python
Overview
In Python web applications, CWE-201 vulnerabilities commonly occur when developers inadvertently expose sensitive information in HTTP responses, error messages, logs, or API responses. Python's dynamic nature and popular frameworks like Django, Flask, and FastAPI make it easy to serialize entire objects or return detailed error traces, which can leak passwords, tokens, internal paths, PII, and system configuration details.
Python applications are particularly susceptible when using ORM models directly in API responses (exposing all database fields), returning exception stack traces in production, or logging sensitive data through Python's logging module. Django's DEBUG mode, Flask's debug mode, and improper exception handling in FastAPI can all expose detailed system information to unauthorized users.
Common scenarios include: serializing Django/SQLAlchemy models without field filtering, returning __dict__ attributes of user objects, exposing environment variables in error responses, and including sensitive data in log files that may be accessible to attackers. Modern frameworks provide protections, but developers must explicitly configure them and use Data Transfer Objects (DTOs) or serializers to control exactly what data is transmitted.
Primary Defence: Use Data Transfer Objects (DTOs) or Django REST Framework serializers with explicit field allowlists to control exposed data instead of serializing ORM models directly, implement centralized error handlers that return generic messages while logging full details server-side with exc_info=True, configure Python's logging module with custom filters to redact sensitive fields (passwords, tokens, credit cards), and ensure DEBUG=False in production with secure Actuator/admin panel configurations.
This guidance demonstrates how to prevent information disclosure in Python applications using proper error handling, response filtering, DTO patterns, and secure logging practices across Flask, Django, and FastAPI frameworks.
Common Vulnerable Patterns
Direct ORM Model Serialization
# VULNERABLE - Exposing entire database model including sensitive fields
from flask import Flask, jsonify
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String)
email = Column(String)
password_hash = Column(String) # SENSITIVE!
reset_token = Column(String) # SENSITIVE!
api_key = Column(String) # SENSITIVE!
is_admin = Column(Integer) # INTERNAL!
app = Flask(__name__)
@app.route('/api/user/<int:user_id>')
def get_user(user_id):
user = session.query(User).get(user_id)
# Returns ALL fields including password_hash, reset_token, api_key!
return jsonify(user.__dict__)
# Attack result:
# {
# "id": 123,
# "username": "john",
# "email": "john@example.com",
# "password_hash": "$2b$12$abc123...", ← EXPOSED!
# "reset_token": "secret-token-12345", ← EXPOSED!
# "api_key": "sk-1234567890abcdef", ← EXPOSED!
# "is_admin": 1 ← INTERNAL INFO!
# }
Stack Traces in Production Error Responses
# VULNERABLE - Exposing internal paths, code structure, and environment details
from flask import Flask, jsonify
import traceback
app = Flask(__name__)
@app.route('/api/process')
def process_data():
try:
# Some complex operation
result = perform_database_query()
return jsonify(result)
except Exception as e:
# Exposes full stack trace with file paths, code, environment
return jsonify({
'error': str(e),
'type': type(e).__name__,
'traceback': traceback.format_exc() # DANGEROUS!
}), 500
# Attack result when error occurs:
# {
# "error": "FATAL: password authentication failed for user 'admin'",
# "type": "OperationalError",
# "traceback": "Traceback (most recent call last):
# File '/home/deploy/myapp/app.py', line 45, in process_data
# result = perform_database_query()
# File '/home/deploy/myapp/db.py', line 123, in perform_database_query
# conn = psycopg2.connect('host=10.0.1.5 user=admin password=secret123')
# ..."
# }
# ← Exposes internal paths, database credentials, IP addresses!
Detailed Login Error Messages
# VULNERABLE - Enables user enumeration and reveals password validation logic
from flask import Flask, request, jsonify
from werkzeug.security import check_password_hash
app = Flask(__name__)
@app.route('/api/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
user = User.query.filter_by(username=username).first()
# Reveals whether username exists
if not user:
return jsonify({
'error': f'No user found with username: {username}' # USER ENUMERATION!
}), 404
# Reveals password validation details
if not check_password_hash(user.password_hash, password):
return jsonify({
'error': f'Invalid password for user {username}',
'hash': user.password_hash, # EXPOSES PASSWORD HASH!
'hint': 'Password must be at least 8 characters'
}), 401
return jsonify({'token': generate_token(user)})
# Attack result for enumeration:
# Try: {"username": "admin"}
# Response: "No user found with username: admin" vs "Invalid password"
# Attacker now knows which usernames exist!
Sensitive Data in Logs
# VULNERABLE - Logging sensitive data that can be accessed by attackers
import logging
from flask import Flask, request, jsonify
app = Flask(__name__)
logging.basicConfig(level=logging.DEBUG, filename='app.log')
@app.route('/api/payment', methods=['POST'])
def process_payment():
data = request.json
# Logs sensitive payment information
logging.info(f'Processing payment: {data}') # LOGS CREDIT CARDS!
# Logs user credentials
logging.debug(f'User: {data["username"]}, Password: {data["password"]}') # DANGEROUS!
try:
result = charge_card(data['card_number'], data['cvv'])
return jsonify({'status': 'success'})
except Exception as e:
# Logs full request with sensitive data
logging.error(f'Payment failed for request: {request.json}') # LOGS SENSITIVE DATA!
return jsonify({'error': 'Payment processing failed'}), 500
# app.log will contain:
# INFO: Processing payment: {'username': 'john', 'password': 'secret123',
# 'card_number': '4111111111111111', 'cvv': '123'}
# ← All sensitive data exposed in log files!
Environment Variables in Error Responses
# VULNERABLE - Exposing configuration and secrets in debug pages
from flask import Flask, jsonify
import os
app = Flask(__name__)
app.config['DEBUG'] = True # DANGEROUS in production!
@app.route('/api/config')
def get_config():
# Exposes all environment variables including secrets
return jsonify({
'env': dict(os.environ), # EXPOSES ALL SECRETS!
'config': dict(app.config) # EXPOSES APP SECRETS!
})
# Attack result:
# {
# "env": {
# "DATABASE_URL": "postgres://admin:password123@db.internal.com:5432/prod",
# "SECRET_KEY": "super-secret-key-12345",
# "AWS_ACCESS_KEY": "AKIAIOSFODNN7EXAMPLE",
# "AWS_SECRET_KEY": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
# ...
# }
# }
# ← All credentials and secrets exposed!
Secure Patterns
DTO Pattern with Explicit Field Allowlist
# SECURE - Using Data Transfer Objects to control exposed fields
from flask import Flask, jsonify
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from dataclasses import dataclass
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String)
email = Column(String)
password_hash = Column(String)
reset_token = Column(String)
api_key = Column(String)
is_admin = Column(Integer)
# DTO with only safe fields
@dataclass
class UserDTO:
id: int
username: str
email: str
# NEVER include: password_hash, reset_token, api_key, is_admin
@classmethod
def from_model(cls, user: User):
return cls(
id=user.id,
username=user.username,
email=user.email
)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email
}
app = Flask(__name__)
@app.route('/api/user/<int:user_id>')
def get_user(user_id):
user = session.query(User).get(user_id)
if not user:
return jsonify({'error': 'User not found'}), 404
# Convert to DTO - only returns safe fields
user_dto = UserDTO.from_model(user)
return jsonify(user_dto.to_dict())
Why this works: The Data Transfer Object (DTO) pattern creates an explicit allowlist of safe fields that can be exposed in API responses, preventing accidental disclosure when database models are extended with new sensitive columns. By using @dataclass with explicit field definitions, the code gains compile-time type safety and IDE support. The from_model() class method provides a single, auditable location where ORM-to-DTO conversion happens, making it easy to verify that sensitive fields like password_hash, reset_token, and api_key are never included. Unlike directly serializing user.__dict__ which exposes all attributes, the DTO pattern requires developers to consciously decide which fields to include, following the principle of least privilege for data exposure.
Generic Error Handling with Server-Side Logging
# SECURE - Generic user errors with detailed server-side logging
from flask import Flask, jsonify
import logging
from typing import Optional
app = Flask(__name__)
# Configure logging to file only (not to response)
logging.basicConfig(
level=logging.INFO,
filename='/var/log/app/application.log',
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class ErrorResponse:
"""Standardized error response format"""
@staticmethod
def create(error_code: str, message: str, status_code: int = 500):
return jsonify({
'error': message,
'error_code': error_code
}), status_code
@app.route('/api/process')
def process_data():
try:
result = perform_database_query()
return jsonify(result)
except Exception as e:
# Log full error details server-side (with stack trace)
logger.error(
'Database query failed',
exc_info=True, # Includes full traceback in log
extra={'user_id': get_current_user_id()}
)
# Return generic error to client
return ErrorResponse.create(
error_code='DB_ERR_001',
message='Database operation failed',
status_code=500
)
# Error handling for different exception types
@app.errorhandler(ValueError)
def handle_value_error(e):
logger.warning(f'Invalid input: {e}', exc_info=True)
return ErrorResponse.create(
error_code='INVALID_INPUT',
message='Invalid request data',
status_code=400
)
@app.errorhandler(Exception)
def handle_generic_error(e):
logger.error('Unhandled exception', exc_info=True)
return ErrorResponse.create(
error_code='INTERNAL_ERROR',
message='An unexpected error occurred',
status_code=500
)
Why this works: This pattern separates error visibility into two channels: detailed server-side logging (with exc_info=True to capture full stack traces) and generic client-facing error messages. The ErrorResponse.create() standardized response format ensures consistency and prevents accidental detail leakage. By logging with exc_info=True, developers get complete diagnostic information including file paths, line numbers, and variable states for debugging, while clients receive only safe, generic messages like "Database operation failed". The error code system (DB_ERR_001, INVALID_INPUT) enables error tracking and correlation in logs without exposing implementation details. This approach prevents information disclosure of internal paths, database schema, environment details, and code structure that attackers could use for reconnaissance.
Secure Login with Generic Error Messages
# SECURE - Preventing user enumeration with generic error messages
from flask import Flask, request, jsonify
from werkzeug.security import check_password_hash
import logging
import time
app = Flask(__name__)
logger = logging.getLogger(__name__)
@app.route('/api/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
# Generic error message for all failure cases
GENERIC_ERROR = {'error': 'Invalid credentials'}
# Input validation
if not username or not password:
return jsonify(GENERIC_ERROR), 401
user = User.query.filter_by(username=username).first()
# Log attempt server-side (for security monitoring)
if not user:
logger.warning(f'Login attempt for non-existent user: {username}')
# Constant-time response to prevent timing attacks
check_password_hash('$2b$12$dummy_hash_for_timing', password)
return jsonify(GENERIC_ERROR), 401
# Check password
if not check_password_hash(user.password_hash, password):
logger.warning(f'Failed login attempt for user: {username}')
return jsonify(GENERIC_ERROR), 401
# Success - return only safe data
token = generate_token(user.id)
logger.info(f'Successful login for user: {username}')
return jsonify({
'token': token,
'user': {
'id': user.id,
'username': user.username
# NO password_hash, api_key, or other sensitive fields
}
})
Why this works: Returning identical error messages ("Invalid credentials") for both non-existent users and incorrect passwords prevents user enumeration attacks where attackers probe for valid usernames. The dummy hash check (check_password_hash('$2b$12$dummy_hash_for_timing', password)) when the user doesn't exist ensures constant-time responses, preventing timing attacks that could distinguish between "user not found" (fast) and "wrong password" (slow hash verification). Server-side logging with logger.warning() captures failed login attempts for security monitoring without exposing this information to clients. The successful response returns only safe fields (id, username, token), never including password_hash, api_key, is_admin, or other sensitive attributes. This defense-in-depth approach protects against account enumeration, timing analysis, and accidental credential exposure.
Secure Logging with Sensitive Data Filtering
# SECURE - Filtering sensitive data from logs
from flask import Flask, request, jsonify
import logging
import re
from typing import Any, Dict
app = Flask(__name__)
class SensitiveDataFilter(logging.Filter):
"""Custom filter to redact sensitive information from logs"""
SENSITIVE_PATTERNS = {
'password': re.compile(r'(["\']?password["\']?\s*[:=]\s*["\']?)([^"\'}\s,]+)', re.IGNORECASE),
'token': re.compile(r'(["\']?token["\']?\s*[:=]\s*["\']?)([^"\'}\s,]+)', re.IGNORECASE),
'api_key': re.compile(r'(["\']?api_?key["\']?\s*[:=]\s*["\']?)([^"\'}\s,]+)', re.IGNORECASE),
'credit_card': re.compile(r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b'),
'cvv': re.compile(r'(["\']?cvv["\']?\s*[:=]\s*["\']?)(\d{3,4})', re.IGNORECASE),
}
@classmethod
def redact_sensitive_data(cls, message: str) -> str:
"""Redact sensitive data from log message"""
redacted = message
for pattern_name, pattern in cls.SENSITIVE_PATTERNS.items():
if pattern_name == 'credit_card':
redacted = pattern.sub(r'XXXX-XXXX-XXXX-XXXX', redacted)
else:
redacted = pattern.sub(r'\1[REDACTED]', redacted)
return redacted
def filter(self, record):
record.msg = self.redact_sensitive_data(str(record.msg))
return True
# Configure logging with sensitive data filtering
handler = logging.FileHandler('/var/log/app/application.log')
handler.addFilter(SensitiveDataFilter())
logger = logging.getLogger(__name__)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
def sanitize_for_logging(data: Dict[str, Any]) -> Dict[str, Any]:
"""Remove sensitive fields before logging"""
SENSITIVE_FIELDS = {
'password', 'password_hash', 'token', 'api_key',
'secret', 'credit_card', 'card_number', 'cvv', 'ssn'
}
return {
k: '[REDACTED]' if k.lower() in SENSITIVE_FIELDS else v
for k, v in data.items()
}
@app.route('/api/payment', methods=['POST'])
def process_payment():
data = request.json
# Sanitize before logging
safe_data = sanitize_for_logging(data)
logger.info(f'Processing payment: {safe_data}')
try:
result = charge_card(data['card_number'], data['cvv'])
return jsonify({'status': 'success', 'transaction_id': result['id']})
except Exception as e:
# Log error without sensitive data
logger.error(f'Payment failed for user {data.get("user_id")}', exc_info=True)
return jsonify({'error': 'Payment processing failed'}), 500
Why this works: The SensitiveDataFilter uses regex patterns to automatically redact sensitive information like passwords, tokens, API keys, credit card numbers, and CVVs from log messages before they're written to files. This automatic redaction prevents sensitive data from appearing in logs even if developers accidentally log it. The sanitize_for_logging() function provides an explicit allowlist approach, replacing known sensitive field names with [REDACTED] before logging. This dual-layer protection (pattern-based redaction + field-based allowlist) ensures comprehensive coverage. Logs remain useful for debugging by preserving transaction IDs, user IDs, timestamps, and error context, while credit cards become "XXXX-XXXX-XXXX-XXXX" and passwords become "[REDACTED]". This prevents log file compromise from exposing credentials, payment data, or secrets that attackers could use for account takeover or fraud.
Django REST Framework Serializer Pattern
# SECURE - Using DRF serializers for field control
from rest_framework import serializers, viewsets
from rest_framework.response import Response
from django.contrib.auth.models import User
# Public serializer - only safe fields
class UserPublicSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'date_joined']
# Explicitly exclude: password, is_staff, is_superuser, last_login
read_only_fields = ['id', 'date_joined']
# Admin serializer - more fields for authorized users
class UserAdminSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'date_joined', 'is_active', 'last_login']
# Still exclude: password, is_staff, is_superuser
read_only_fields = ['id', 'date_joined', 'last_login']
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
def get_serializer_class(self):
# Use different serializers based on permission level
if self.request.user.is_staff:
return UserAdminSerializer
return UserPublicSerializer
def retrieve(self, request, pk=None):
user = self.get_object()
serializer = self.get_serializer(user)
return Response(serializer.data)
# settings.py - Production security settings
DEBUG = False # Never True in production!
SECRET_KEY = os.environ.get('SECRET_KEY') # Never hardcode
# Custom exception handler for production
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
# Remove detailed error messages in production
response.data = {
'error': 'An error occurred',
'error_code': f'ERR_{response.status_code}'
}
return response
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myapp.custom_exception_handler',
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
Why this works: Django REST Framework's ModelSerializer with explicit fields lists creates a strict allowlist that prevents accidental exposure when models are modified. The Meta.fields declaration requires developers to consciously choose which fields to expose, and any new database columns are excluded by default. Using different serializers (UserPublicSerializer vs UserAdminSerializer) based on permission levels implements role-based data exposure, ensuring regular users never see administrative fields. Setting DEBUG=False in production prevents Django's detailed error pages from exposing SQL queries, file paths, settings, and environment variables. The custom exception handler (custom_exception_handler) replaces DRF's detailed error responses with generic messages, preventing stack traces and internal details from reaching clients. The read_only_fields configuration prevents clients from modifying sensitive fields even if they somehow appear in requests. This framework-level approach ensures consistent security across all API endpoints.
FastAPI with Pydantic Models
# SECURE - Using Pydantic for response validation
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, EmailStr
from sqlalchemy.orm import Session
import logging
app = FastAPI()
logger = logging.getLogger(__name__)
# Database model (ORM)
class UserDB:
def __init__(self, id, username, email, password_hash, api_key):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash # NEVER expose this
self.api_key = api_key # NEVER expose this
# Response model - only public fields
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
# Explicitly exclude: password_hash, api_key
class Config:
orm_mode = True
@classmethod
def from_orm_safe(cls, user: UserDB):
"""Safely convert ORM model to response"""
return cls(
id=user.id,
username=user.username,
email=user.email
)
@app.get('/api/user/{user_id}', response_model=UserResponse)
async def get_user(user_id: int):
user = db.query(UserDB).get(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='User not found' # Generic message
)
# Pydantic ensures only defined fields are returned
return UserResponse.from_orm_safe(user)
# Global exception handler
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
# Log full error server-side
logger.error(f'Unhandled exception: {exc}', exc_info=True)
# Return generic error to client
return JSONResponse(
status_code=500,
content={
'error': 'Internal server error',
'error_code': 'INTERNAL_ERROR'
}
)
Why this works: Pydantic's response_model enforces a strict field allowlist, automatically excluding any fields not defined in the response schema. Explicit conversion from ORM models to Pydantic response models with type validation prevents accidental field exposure. FastAPI's automatic validation and generic error messages with server-side logging ensure sensitive data never reaches client responses.
Testing Strategies
Testing for information disclosure requires understanding your application's specific data model and risk areas. These test patterns demonstrate verification approaches, but you'll need to adapt them to your framework, ORM, and domain objects.
Key testing principles:
- Test actual serialized output - Verify JSON responses don't contain sensitive fields
- Trigger error conditions - Ensure exceptions don't expose stack traces or internal details
- Validate authentication consistency - Login errors should be identical regardless of failure reason
- Inspect logs programmatically - Capture log output to verify sensitive data redaction
- Test model transformations - Verify DTOs/schemas truly exclude sensitive ORM fields
- Check configuration - Ensure DEBUG mode is off, admin panels are secured
Framework-specific considerations:
- Django: Test
serializers.ModelSerializerfields, verify admin panel security, check DEBUG=False in production - Flask: Test response schemas, validate error handlers, verify session cookie security
- FastAPI: Test Pydantic
response_modeleffectiveness, validate automatic docs exclusions - SQLAlchemy: Verify hybrid properties don't leak sensitive data, test relationship lazy loading
The following examples demonstrate common verification patterns that apply across Python web frameworks.
# SECURE - Comprehensive tests for information disclosure prevention
import pytest
from flask import Flask, jsonify
import json
import re
def test_user_response_no_sensitive_fields():
"""Test that user API responses don't contain sensitive fields"""
# Arrange
response = client.get('/api/user/123')
data = response.get_json()
# Assert - check sensitive fields are NOT present
assert 'password' not in data
assert 'password_hash' not in data
assert 'api_key' not in data
assert 'reset_token' not in data
assert 'secret' not in data
# Assert - check safe fields ARE present
assert 'id' in data
assert 'username' in data
assert 'email' in data
def test_error_response_no_stack_trace():
"""Test that error responses don't expose stack traces"""
# Arrange - trigger an error
response = client.post('/api/process', json={'invalid': 'data'})
data = response.get_json()
# Assert - no sensitive error details
assert 'traceback' not in json.dumps(data).lower()
assert 'stack' not in json.dumps(data).lower()
assert '/home/' not in json.dumps(data) # No file paths
assert '.py' not in json.dumps(data) # No Python filenames
# Assert - has generic error
assert 'error' in data
assert 'error_code' in data
def test_login_no_user_enumeration():
"""Test that login doesn't reveal which usernames exist"""
# Test with non-existent user
response1 = client.post('/api/login', json={
'username': 'nonexistent_user_12345',
'password': 'anything'
})
# Test with existing user but wrong password
response2 = client.post('/api/login', json={
'username': 'existing_user',
'password': 'wrong_password'
})
# Both should return same generic error
assert response1.status_code == response2.status_code == 401
assert response1.get_json() == response2.get_json()
assert 'Invalid credentials' in response1.get_json()['error']
def test_logs_no_sensitive_data(caplog):
"""Test that logs don't contain sensitive information"""
# Arrange
with caplog.at_level(logging.INFO):
client.post('/api/payment', json={
'username': 'testuser',
'password': 'secret123',
'card_number': '4111111111111111',
'cvv': '123'
})
# Assert - sensitive data is redacted
log_output = caplog.text
assert 'secret123' not in log_output
assert '4111111111111111' not in log_output
assert '123' not in log_output or 'REDACTED' in log_output
assert '[REDACTED]' in log_output or 'XXXX-XXXX-XXXX-XXXX' in log_output
def test_serializer_field_allowlist():
"""Test that serializers only include approved fields"""
# Arrange
from myapp.serializers import UserPublicSerializer
from myapp.models import User
user = User(
id=1,
username='testuser',
email='test@example.com',
password_hash='$2b$12$abc123',
api_key='secret-key-12345'
)
# Act
serializer = UserPublicSerializer(user)
data = serializer.data
# Assert - only safe fields present
assert set(data.keys()) == {'id', 'username', 'email', 'date_joined'}
assert 'password_hash' not in data
assert 'api_key' not in data
def test_dto_conversion_safe():
"""Test that DTO conversion filters sensitive fields"""
# Arrange
from myapp.dto import UserDTO
user = MockUser(
id=1,
username='test',
email='test@example.com',
password_hash='hash123',
reset_token='token456'
)
# Act
dto = UserDTO.from_model(user)
data = dto.to_dict()
# Assert
assert 'password_hash' not in data
assert 'reset_token' not in data
assert data['username'] == 'test'
def test_environment_variables_not_exposed():
"""Test that environment variables are not exposed in responses"""
# Arrange - set sensitive env var
import os
os.environ['SECRET_KEY'] = 'super-secret-value'
# Act - call various endpoints
response = client.get('/api/config')
# Assert - secret not in response
response_text = json.dumps(response.get_json())
assert 'super-secret-value' not in response_text
assert 'SECRET_KEY' not in response_text or 'REDACTED' in response_text
# Test cases cover:
# 1. Field filtering in API responses
# 2. Error message sanitization
# 3. User enumeration prevention
# 4. Log redaction
# 5. Serializer field control
# 6. DTO conversion safety
# 7. Environment variable protection