Skip to content
intermediate Phase 8 · Application Security and Secure Coding

Error Handling and Information Leakage

Return generic failures to users while logging detail securely.

45m
0 problems
Topic Progress 0%

How Error Messages Leak Sensitive Information

Information Leakage: When Error Messages Become an Attacker's Guide

Information leakage occurs when an application reveals sensitive internal details — stack traces, database errors, file paths, configuration values, or framework versions — in responses visible to users or attackers. These details provide a roadmap for further exploitation.

What Leaks and Why It Matters

Stack Traces

A stack trace reveals the application's internal structure: file paths, function names, variable values, and the exact line where the error occurred. This tells the attacker:

  • Which framework and version is in use (enabling targeted exploit development)
  • The application's directory structure (enabling path traversal attacks)
  • The flow of execution (enabling logic flaw discovery)
HTTP/1.1 500 Internal Server Error

Traceback (most recent call last):
  File "/app/routes/user.py", line 42, in get_user
    user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
  File "/app/db/connection.py", line 15, in query
    cursor.execute(sql)
sqlalchemy.exc.ProgrammingError: (psycopg2.errors.SyntaxError) syntax error at or near "WHERE"

This stack trace reveals: the framework (SQLAlchemy), the database (PostgreSQL), the file structure, and the vulnerable query pattern.

Database Errors

Database errors often include table names, column names, and query fragments:

ERROR: relation "usr" does not exist

This tells the attacker the table is named users (not usr), and they can refine their SQL injection payload.

Framework and Version Disclosure

HTTP headers often reveal the server software and version:

Server: Apache/2.4.41 (Ubuntu)
X-Powered-By: Express/4.17.1

An attacker can look up known vulnerabilities for these exact versions.

File Paths

Errors that include file paths reveal the server's directory structure:

FileNotFoundError: [Errno 2] No such file or directory: '/var/www/app/config/database.yml'

This tells the attacker the application root is /var/www/app/ and that a config file exists at config/database.yml.

The Attacker's Workflow

Information leakage feeds into every phase of an attack:

  1. Reconnaissance: Error messages reveal the technology stack, directory structure, and configuration details.
  2. Vulnerability discovery: Stack traces point to the exact code location of a flaw.
  3. Exploitation: Database errors refine injection payloads. File paths enable path traversal.
  4. Post-exploitation: Verbose errors during lateral movement reveal internal service names and configurations.

Real-World Impact

In 2017, the Equifax breach was partially attributed to an Apache Struts vulnerability. While the vulnerability itself was the entry point, verbose error messages during the attack likely helped attackers understand the internal application structure and pivot to other systems.

Secure Error Handling and Logging Strategy

Designing Secure Error Responses and Effective Logging

The goal of secure error handling is to provide useful information to legitimate users and developers while revealing nothing useful to attackers. This requires a deliberate separation between client-facing error messages and server-side diagnostic logs.

Generic Error Responses

Return the same generic error message regardless of the underlying cause:

# SECURE — generic error response
@app.errorhandler(500)
def internal_error(error):
    # Log the full error details server-side
    logger.error(f"Internal error: {error}", exc_info=True)
    
    # Return a generic message to the client
    return jsonify({
        'error': 'An internal error occurred',
        'reference': g.correlation_id
    }), 500

The client sees: {"error": "An internal error occurred", "reference": "abc-123-def"}

The server log contains the full stack trace, request details, and the correlation ID abc-123-def.

Correlation IDs

A correlation ID is a unique identifier assigned to each request. It appears in the client-facing error response and in all server-side log entries for that request. This allows support teams to trace a generic error to the full diagnostic details:

import uuid
from flask import g, request

@app.before_request
def assign_correlation_id():
    g.correlation_id = request.headers.get('X-Correlation-ID', str(uuid.uuid4()))

@app.after_request
def add_correlation_id_header(response):
    response.headers['X-Correlation-ID'] = g.correlation_id
    return response

When a user reports an error, they provide the correlation ID from the error response. The support team searches logs for that ID and finds the full stack trace, request parameters, and execution context.

What to Log (and What Not to Log)

Log:

  • Request method, path, and correlation ID
  • User ID (if authenticated)
  • Error type and stack trace
  • Request parameters (excluding sensitive fields)
  • Response status code and duration
  • Security-relevant events (authentication failures, authorization denials, rate limit triggers)

Never log:

  • Passwords or password hashes
  • Session tokens or API keys
  • Credit card numbers or financial data
  • Personally identifiable information (PII) beyond what is necessary
  • Full request bodies containing sensitive data
# SECURE logging with sensitive data redaction
import copy

def sanitize_for_logging(data):
    sensitive_fields = {'password', 'token', 'credit_card', 'ssn', 'api_key'}
    sanitized = copy.deepcopy(data)
    for field in sensitive_fields:
        if field in sanitized:
            sanitized[field] = '[REDACTED]'
    return sanitized

logger.info(f"Request: {sanitize_for_logging(request.json)}")

Structured Logging

Use structured logging (JSON format) to make log analysis automated and consistent:

import json
import logging

class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_entry = {
            'timestamp': self.formatTime(record),
            'level': record.levelname,
            'message': record.getMessage(),
            'correlation_id': getattr(record, 'correlation_id', None),
            'user_id': getattr(record, 'user_id', None),
            'path': getattr(record, 'path', None),
            'method': getattr(record, 'method', None),
            'status_code': getattr(record, 'status_code', None),
        }
        return json.dumps(log_entry)

Structured logs can be ingested by SIEM (Security Information and Event Management) systems for automated alerting on security events.

Practice: Fix Information Leakage

The following code leaks sensitive information. Identify the issues and fix them:

@app.route('/api/users/<user_id>')
def get_user(user_id):
    try:
        user = db.execute(f"SELECT * FROM users WHERE id = {user_id}").fetchone()
        if not user:
            return jsonify({'error': f'User {user_id} not found in table users'}), 404
        return jsonify(user)
    except Exception as e:
        return jsonify({'error': str(e), 'traceback': traceback.format_exc()}), 500

Issues:

  1. The 404 error reveals the table name (users)
  2. The 500 error returns the full exception message and stack trace to the client
  3. The SQL query is also vulnerable to injection (f-string)

Fix:

@app.route('/api/users/<user_id>')
def get_user(user_id):
    try:
        user = db.execute("SELECT * FROM users WHERE id = %s", (user_id,)).fetchone()
        if not user:
            return jsonify({'error': 'User not found', 'reference': g.correlation_id}), 404
        return jsonify(user)
    except Exception as e:
        logger.error(f"Error fetching user {user_id}: {e}", exc_info=True)
        return jsonify({'error': 'An internal error occurred', 'reference': g.correlation_id}), 500

Practice Problems

0 / 2 solved
Identify Information Leakage

The following HTTP response contains information leakage. Identify all leaked information and explain how an attacker could use it. ``` HTTP/1.1 500 Internal Server Error Server: nginx/1.18.0 X-Powered-By: PHP/7.4.3 Fatal error: Uncaught PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'prod_db.usr' doesn't exist in /var/www/html/api/search.php:42 Stack trace: #0 /var/www/html/api/search.php(42): PDO->query('SELECT * FROM u...') #1 /var/www/html/api/index.php(15): include('/var/www/html...') #2 {main} thrown in /var/www/html/api/search.php on line 42 ```

Solution
Leaked information:
1. **Server software and version**: `nginx/1.18.0` and `PHP/7.4.3` — attacker can look up known CVEs for these versions
2. **Database type**: PDO with MySQL error — confirms MySQL/MariaDB
3. **Database name**: `prod_db` — reveals production database naming convention
4. **Table name**: The error says `usr` doesn't exist, implying the correct name is `users` — helps SQL injection
5. **File paths**: `/var/www/html/api/search.php` and `/var/www/html/api/index.php` — reveals directory structure
6. **Line numbers**: Line 42 in search.php, line 15 in index.php — helps attacker understand code flow
7. **Query fragment**: `SELECT * FROM u...` — confirms the query structure

An attacker can use this to: target known vulnerabilities in nginx 1.18.0 and PHP 7.4.3, refine SQL injection payloads knowing the table is `users`, and attempt path traversal using the revealed directory structure.
Implement Correlation IDs

Implement a correlation ID system for a Flask application that: 1. Generates a unique ID for each request 2. Includes the ID in error responses 3. Includes the ID in all log entries for that request

Solution
```python
import uuid
import logging
from flask import Flask, g, request, jsonify

app = Flask(__name__)

@app.before_request
def assign_correlation_id():
    g.correlation_id = request.headers.get('X-Correlation-ID', str(uuid.uuid4()))

@app.after_request
def add_correlation_id_header(response):
    response.headers['X-Correlation-ID'] = g.correlation_id
    return response

class CorrelationIdFilter(logging.Filter):
    def filter(self, record):
        record.correlation_id = getattr(g, 'correlation_id', 'N/A')
        return True

logger = logging.getLogger(__name__)
logger.addFilter(CorrelationIdFilter())

@app.errorhandler(500)
def internal_error(error):
    logger.error(f"Internal error: {error}", exc_info=True)
    return jsonify({
        'error': 'An internal error occurred',
        'reference': g.correlation_id
    }), 500

@app.route('/api/example')
def example():
    logger.info(f"Processing request from {request.remote_addr}")
    # ...
    return jsonify({'status': 'ok'})
```

The correlation ID appears in the response header (`X-Correlation-ID`), in error responses (`reference` field), and in all log entries for that request.

Quiz

1. What is the primary risk of returning stack traces in HTTP error responses?

Question 1 options

2. What is the purpose of a correlation ID in error handling?

Question 2 options

3. Which of the following should NEVER be written to application logs?

Question 3 options

4. A database error message says `Table 'prod_db.usr' doesn't exist`. What information does this leak?

Question 4 options

5. What is the recommended approach for client-facing error messages?

Question 5 options

Flashcards

Question

What information do stack traces leak in error responses?

Answer

File paths, function names, variable values, framework and version information, and the exact line where the error occurred. This reveals the application's internal structure and helps attackers develop targeted exploits.

Question

What is a correlation ID?

Answer

A unique identifier assigned to each request that appears in both the client-facing error response and server-side logs. It allows support teams to trace a generic error to detailed diagnostic information.

Question

What should never be written to application logs?

Answer

Passwords, session tokens, API keys, credit card numbers, and other secrets. Logs are stored for extended periods and may be accessible to operations teams or third-party services. Use redaction to sanitize sensitive fields.

Question

What is the recommended approach for client-facing error messages?

Answer

Return the same generic error message regardless of the underlying cause, with a correlation ID for support. Detailed diagnostics belong in protected server-side logs, never in the HTTP response body.

Question

What does a database error like `Table 'prod_db.usr' doesn't exist` leak?

Answer

The database name (prod_db), the incorrect table name (usr, implying the correct name is users), and the database type (MySQL/MariaDB). This helps attackers refine SQL injection payloads.

Revision Notes

Key Takeaways

  • 1. Stack traces and verbose errors leak internal structure, file paths, framework versions, and code flow
  • 2. Client-facing error responses should be generic — same message regardless of the underlying cause
  • 3. Correlation IDs bridge the gap between generic client errors and detailed server-side logs
  • 4. Detailed diagnostics belong in protected logs, never in the HTTP response body
  • 5. Never log passwords, tokens, or other secrets — use redaction to sanitize sensitive fields

Interview Tips

  • • Walk through a concrete example of how a stack trace helps an attacker develop a targeted exploit
  • • Explain the correlation ID pattern and how it balances security (generic client errors) with debuggability (detailed server logs)
  • • Discuss what should and should not be logged, with specific examples of sensitive data that must be redacted
  • • Connect information leakage to the OWASP Top 10 (A09: Security Logging and Monitoring Failures) and explain how it enables further attacks

Cheat Sheet

Error Handling and Information Leakage Cheat Sheet

Phase: Application Security and Secure Coding | Level: intermediate

What Leaks:

  • Stack traces: file paths, function names, framework versions, code flow
  • Database errors: table names, column names, query fragments
  • HTTP headers: server software and version (Server, X-Powered-By)
  • File paths: directory structure

Defense:

  • Generic error responses for users (same message regardless of cause)
  • Detailed diagnostics in protected server-side logs only
  • Correlation IDs to trace client errors to server logs
  • Structured logging (JSON) for automated analysis
  • Sensitive data redaction before logging

Log:

  • Request method, path, correlation ID, user ID, error type, stack trace
  • Security events: auth failures, authorization denials, rate limits

Never Log:

  • Passwords, session tokens, API keys, credit card numbers, PII

Key points:

  • Stack traces and SQL errors in responses reveal internals attackers can pivot on
  • Correlation IDs let support teams trace a generic client error to detailed server logs
  • Detailed diagnostics belong in protected logs, never in the HTTP response body

Tags: secure-coding, logging

Continue Learning

Before You Start

  • No prerequisites listed.