Skip to content

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:

  1. Test actual serialized output - Verify JSON responses don't contain sensitive fields
  2. Trigger error conditions - Ensure exceptions don't expose stack traces or internal details
  3. Validate authentication consistency - Login errors should be identical regardless of failure reason
  4. Inspect logs programmatically - Capture log output to verify sensitive data redaction
  5. Test model transformations - Verify DTOs/schemas truly exclude sensitive ORM fields
  6. Check configuration - Ensure DEBUG mode is off, admin panels are secured

Framework-specific considerations:

  • Django: Test serializers.ModelSerializer fields, verify admin panel security, check DEBUG=False in production
  • Flask: Test response schemas, validate error handlers, verify session cookie security
  • FastAPI: Test Pydantic response_model effectiveness, 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

Additional Resources