Server-generated Error Message Containing Sensitive Information

Description

Server-generated Error Message Containing Sensitive Information is a vulnerability where error messages produced by web servers, application servers, or backend systems reveal sensitive data to users or attackers. While error messages themselves serve a legitimate purpose, they become security vulnerabilities when they expose information that attackers can use for reconnaissance or exploitation. This includes server versions, internal paths, database details, configuration settings, stack traces, and other technical information that should remain hidden from end users.

Risk

Server-generated error messages pose significant reconnaissance risks. HTTP error pages may reveal web server software and version numbers, enabling targeted exploits. Database errors can expose query structures, table names, and even data values. Application errors may show full file paths revealing the system's directory structure. Stack traces expose class names, method signatures, and line numbers that reveal application architecture. Configuration errors might show connection strings, hostnames, or authentication details. Attackers systematically trigger errors to map the backend infrastructure and identify specific vulnerabilities to exploit.

Solution

Implement consistent error handling that provides meaningful feedback to legitimate users while restricting attacker-useful information. Configure web servers to display custom error pages that hide technical details. Ensure application code catches all exceptions and returns generic error messages. Log detailed error information server-side for debugging while presenting sanitized messages to users. Disable verbose error output and debug modes in production. Test error handling by deliberately triggering various error conditions to verify no sensitive information leaks.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Attackers can extract sensitive system information from error messages, including server versions, paths, database details, and configuration settings.

Example Code

Vulnerable Code

// Vulnerable: Servlet returning server-generated errors
public class VulnerableServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String filename = request.getParameter("file");

        try {
            // Vulnerable: Database error exposes connection details
            Connection conn = DriverManager.getConnection(
                "jdbc:mysql://db.internal:3306/app", "admin", "password");
            // ...
        } catch (SQLException e) {
            // Vulnerable: Raw exception sent to client
            response.sendError(500, "Database error: " + e.getMessage());
            // Output: Database error: Communications link failure to db.internal:3306
        }

        try {
            // Vulnerable: File error reveals path structure
            FileInputStream fis = new FileInputStream("/app/data/" + filename);
        } catch (FileNotFoundException e) {
            // Vulnerable: Full path in error
            throw new ServletException(e);
            // Output: java.io.FileNotFoundException: /app/data/config.xml
        }
    }
}
<?php
// Vulnerable: PHP exposing server errors
// php.ini: display_errors = On (default in development)

function getUserData($userId) {
    // Vulnerable: Database error exposes query
    $conn = mysqli_connect("localhost", "root", "password", "users");
    $result = mysqli_query($conn, "SELECT * FROM users WHERE id = '$userId'");

    if (!$result) {
        // Vulnerable: MySQL error with query details
        die("Database error: " . mysqli_error($conn));
        // Output: Database error: You have an error in your SQL syntax;
        // check the manual... near 'SELECT * FROM users WHERE id = '1' OR '1'='1''
    }

    return mysqli_fetch_assoc($result);
}

// Vulnerable: File operation error
function readConfig($configFile) {
    $content = file_get_contents("/var/www/config/" . $configFile);
    if ($content === false) {
        // Vulnerable: PHP warning reveals path
        trigger_error("Failed to read config file", E_USER_WARNING);
        // Output: Warning: file_get_contents(/var/www/config/db.ini): failed
        // to open stream: Permission denied in /var/www/html/config.php on line 15
    }
    return $content;
}
?>
# Vulnerable: Flask application with debug mode
from flask import Flask, request
import traceback

app = Flask(__name__)
app.debug = True  # Vulnerable: Debug mode in production

@app.route('/api/user/<user_id>')
def get_user(user_id):
    try:
        # May raise various exceptions
        user = database.get_user(user_id)
        return jsonify(user)
    except Exception as e:
        # Vulnerable: Full traceback in response
        return f"Error: {traceback.format_exc()}", 500
        # Output includes full stack trace, file paths, line numbers

@app.route('/api/file')
def get_file():
    filename = request.args.get('name')
    # Vulnerable: Raw exception on file errors
    with open(f'/app/files/{filename}', 'r') as f:
        return f.read()
    # FileNotFoundError reveals /app/files/ path structure

@app.errorhandler(500)
def internal_error(e):
    # Vulnerable: Original exception exposed
    return f"Server Error: {str(e)}", 500
// Vulnerable: Express.js with error details exposed
const express = require('express');
const app = express();

app.get('/api/data/:id', async (req, res) => {
    try {
        const data = await database.query(`SELECT * FROM data WHERE id = ${req.params.id}`);
        res.json(data);
    } catch (error) {
        // Vulnerable: Full error object sent to client
        res.status(500).json({
            error: error.message,
            stack: error.stack,
            query: error.sql  // Database query exposed!
        });
    }
});

// Vulnerable: Default Express error handler exposes stack trace
app.use((err, req, res, next) => {
    res.status(500).send(`
        <h1>Server Error</h1>
        <pre>${err.stack}</pre>
    `);
});

// Vulnerable: Production environment detection bypassed
if (process.env.NODE_ENV !== 'production') {
    // But NODE_ENV might not be set!
    app.set('verbose errors', true);
}

Fixed Code

// Fixed: Secure error handling in servlet
public class SecureServlet extends HttpServlet {
    private static final Logger logger = LoggerFactory.getLogger(SecureServlet.class);

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        try {
            String filename = request.getParameter("file");

            // Validate and process...
            Connection conn = getConnection();
            processFile(filename);

            response.getWriter().println("Success");

        } catch (SQLException e) {
            // Fixed: Log details, return generic message
            String errorId = generateErrorId();
            logger.error("Database error [{}]: {}", errorId, e.getMessage(), e);
            sendErrorResponse(response, 500,
                "A database error occurred. Reference: " + errorId);

        } catch (FileNotFoundException e) {
            String errorId = generateErrorId();
            logger.error("File not found [{}]: {}", errorId, e.getMessage());
            sendErrorResponse(response, 404, "Resource not found");

        } catch (Exception e) {
            String errorId = generateErrorId();
            logger.error("Unexpected error [{}]", errorId, e);
            sendErrorResponse(response, 500,
                "An error occurred. Reference: " + errorId);
        }
    }

    private void sendErrorResponse(HttpServletResponse response, int code, String message)
            throws IOException {
        response.setStatus(code);
        response.setContentType("application/json");
        response.getWriter().println("{\"error\": \"" + escapeJson(message) + "\"}");
    }

    private String generateErrorId() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

    private String escapeJson(String text) {
        return text.replace("\"", "\\\"").replace("\n", "\\n");
    }
}
<?php
// Fixed: PHP with secure error handling
// php.ini (production):
// display_errors = Off
// log_errors = On
// error_log = /var/log/php/error.log

class SecureErrorHandler {
    public static function init() {
        // Fixed: Custom error handler
        set_error_handler([self::class, 'handleError']);
        set_exception_handler([self::class, 'handleException']);

        // Fixed: Disable error display
        ini_set('display_errors', 0);
        ini_set('log_errors', 1);
    }

    public static function handleError($severity, $message, $file, $line) {
        $errorId = self::generateErrorId();

        // Fixed: Log full details
        error_log("Error [$errorId]: $message in $file on line $line");

        // Fixed: Throw as exception for uniform handling
        throw new ErrorException($message, 0, $severity, $file, $line);
    }

    public static function handleException($exception) {
        $errorId = self::generateErrorId();

        // Fixed: Log full details
        error_log("Exception [$errorId]: " . $exception->getMessage() .
                  "\n" . $exception->getTraceAsString());

        // Fixed: Generic response to user
        http_response_code(500);
        echo json_encode([
            'error' => 'An error occurred',
            'reference' => $errorId
        ]);
        exit;
    }

    private static function generateErrorId() {
        return bin2hex(random_bytes(4));
    }
}

SecureErrorHandler::init();

function getUserData($userId) {
    try {
        $conn = getSecureConnection();
        $stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$userId]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    } catch (PDOException $e) {
        // Fixed: Let global handler deal with it
        throw new RuntimeException("Unable to retrieve user data");
    }
}

function readConfig($configFile) {
    // Fixed: Validate filename
    if (!preg_match('/^[a-zA-Z0-9_-]+\.ini$/', $configFile)) {
        throw new InvalidArgumentException("Invalid configuration file");
    }

    $path = "/var/www/config/$configFile";
    if (!file_exists($path)) {
        error_log("Config file not found: $path");
        throw new RuntimeException("Configuration not available");
    }

    return file_get_contents($path);
}
?>
# Fixed: Flask application with secure error handling
from flask import Flask, request, jsonify
import logging
import uuid

app = Flask(__name__)
app.debug = False  # Fixed: Debug disabled in production

# Fixed: Configure logging
logging.basicConfig(
    filename='/var/log/app/error.log',
    level=logging.ERROR,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def generate_error_id():
    return str(uuid.uuid4())[:8]

@app.route('/api/user/<user_id>')
def get_user(user_id):
    try:
        user = database.get_user(user_id)
        if not user:
            return jsonify({'error': 'User not found'}), 404
        return jsonify(user)
    except DatabaseError as e:
        error_id = generate_error_id()
        logger.error(f"Database error [{error_id}]: {e}", exc_info=True)
        return jsonify({
            'error': 'Unable to retrieve user data',
            'reference': error_id
        }), 500
    except Exception as e:
        error_id = generate_error_id()
        logger.error(f"Unexpected error [{error_id}]", exc_info=True)
        return jsonify({
            'error': 'An error occurred',
            'reference': error_id
        }), 500

@app.route('/api/file')
def get_file():
    try:
        filename = request.args.get('name')
        # Fixed: Validate filename
        if not filename or not filename.replace('.', '').replace('-', '').replace('_', '').isalnum():
            return jsonify({'error': 'Invalid filename'}), 400

        filepath = os.path.join('/app/files', filename)
        # Fixed: Prevent path traversal
        if not os.path.realpath(filepath).startswith('/app/files/'):
            return jsonify({'error': 'Access denied'}), 403

        with open(filepath, 'r') as f:
            return f.read()
    except FileNotFoundError:
        return jsonify({'error': 'File not found'}), 404
    except Exception as e:
        error_id = generate_error_id()
        logger.error(f"File error [{error_id}]", exc_info=True)
        return jsonify({'error': 'Unable to read file', 'reference': error_id}), 500

# Fixed: Global error handler
@app.errorhandler(Exception)
def handle_exception(e):
    error_id = generate_error_id()
    logger.error(f"Unhandled exception [{error_id}]", exc_info=True)
    return jsonify({
        'error': 'An unexpected error occurred',
        'reference': error_id
    }), 500

@app.errorhandler(500)
def internal_error(e):
    error_id = generate_error_id()
    logger.error(f"Internal error [{error_id}]", exc_info=True)
    return jsonify({
        'error': 'Internal server error',
        'reference': error_id
    }), 500
// Fixed: Express.js with secure error handling
const express = require('express');
const winston = require('winston');
const { v4: uuidv4 } = require('uuid');

const app = express();

// Fixed: Configure logging
const logger = winston.createLogger({
    level: 'error',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ filename: '/var/log/app/error.log' })
    ]
});

function generateErrorId() {
    return uuidv4().substring(0, 8);
}

app.get('/api/data/:id', async (req, res, next) => {
    try {
        // Fixed: Parameterized query
        const data = await database.query('SELECT * FROM data WHERE id = ?', [req.params.id]);
        res.json(data);
    } catch (error) {
        next(error);  // Pass to error handler
    }
});

// Fixed: Secure error handler
app.use((err, req, res, next) => {
    const errorId = generateErrorId();

    // Fixed: Log full details server-side
    logger.error({
        errorId,
        message: err.message,
        stack: err.stack,
        path: req.path,
        method: req.method,
        ip: req.ip
    });

    // Fixed: Generic response to client
    const statusCode = err.statusCode || 500;
    res.status(statusCode).json({
        error: statusCode === 500 ? 'Internal server error' : err.message,
        reference: errorId
    });
});

// Fixed: Ensure NODE_ENV is properly set
if (!process.env.NODE_ENV) {
    console.warn('NODE_ENV not set, defaulting to production');
    process.env.NODE_ENV = 'production';
}

// Fixed: Disable verbose errors regardless
app.set('verbose errors', false);

module.exports = app;

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, server-generated error information disclosure is extremely common in:

  • Misconfigured web applications
  • Default server configurations
  • Development settings left in production

References

  1. MITRE Corporation. "CWE-550: Server-generated Error Message Containing Sensitive Information." https://cwe.mitre.org/data/definitions/550.html
  2. OWASP. "Error Handling Cheat Sheet."
  3. OWASP. "OWASP Top Ten 2021 - A05:2021 Security Misconfiguration."