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
| Impact | Details |
|---|---|
| Confidentiality | Scope: 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
- MITRE Corporation. "CWE-550: Server-generated Error Message Containing Sensitive Information." https://cwe.mitre.org/data/definitions/550.html
- OWASP. "Error Handling Cheat Sheet."
- OWASP. "OWASP Top Ten 2021 - A05:2021 Security Misconfiguration."