Missing Custom Error Page
Description
Missing Custom Error Page is a configuration vulnerability where a web application does not return custom error pages to users, instead displaying default server error pages that may expose sensitive information. When unhandled exceptions occur or errors are triggered, default error pages often include detailed stack traces, framework versions, database information, file paths, and other technical details that help attackers understand the application's internal architecture. This information disclosure enables more targeted attacks against the specific technologies and configurations in use.
Risk
Default error pages create significant reconnaissance opportunities for attackers. Stack traces reveal class names, method names, and code paths that indicate the application's structure. Framework and version information exposes known vulnerabilities in those specific versions. Database error messages may reveal table names, column names, and query structures useful for SQL injection. File paths expose the server's directory structure. Server software identification enables version-specific attacks. Even seemingly innocuous information helps attackers build a comprehensive picture of the target system, making subsequent attacks more efficient and effective.
Solution
Configure custom error pages for all HTTP error codes (400, 401, 403, 404, 500, etc.). Ensure error pages show user-friendly messages without technical details. Log detailed error information server-side for debugging while showing generic messages to users. In ASP.NET, use <customErrors mode="On"> with a defaultRedirect. In Java/J2EE, configure error-page elements in web.xml. In PHP, disable display_errors in production. Test error handling by triggering various error conditions and verifying that no sensitive information is exposed. Include error page configuration in security hardening checklists.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Read Application Data - Error messages expose internal architecture, paths, and configuration details. |
| Access Control | Scope: Access Control Gain Privileges - Disclosed information enables more targeted attacks against known vulnerabilities. |
| Other | Scope: Other Quality Degradation - Unprofessional error pages reduce user confidence in the application. |
Example Code
Vulnerable Code
<!-- Vulnerable: ASP.NET web.config with customErrors Off -->
<configuration>
<system.web>
<!-- Vulnerable: Shows full stack traces to all users -->
<customErrors mode="Off" />
</system.web>
</configuration>
<!-- Default ASP.NET error page exposes:
- Full exception stack trace
- Source code line numbers
- File paths
- .NET Framework version
- Assembly information
-->
<!-- Vulnerable: Java web.xml without error pages -->
<web-app>
<!-- No error-page elements defined -->
<!-- Container shows default error pages with stack traces -->
</web-app>
<!-- Tomcat default error page exposes:
- Java exception class and message
- Full stack trace
- Servlet container version
- JVM version
-->
<?php
// Vulnerable: PHP with display_errors enabled in production
ini_set('display_errors', 1);
error_reporting(E_ALL);
// When error occurs, full details shown to user
$result = $db->query("SELECT * FROM users WHERE id = " . $_GET['id']);
// Error message might show:
// - Database server type and version
// - SQL query structure
// - Table and column names
// - File paths
?>
// Vulnerable: Java servlet without error handling
public class VulnerableServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) {
String userId = request.getParameter("id");
// Vulnerable: SQLException propagates to container
// Default error page shows full stack trace
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT * FROM users WHERE id = " + userId);
// Exception reveals:
// - Database type (MySQL, Oracle, etc.)
// - Query structure
// - JDBC driver version
// - Application class names
}
}
# Vulnerable: Flask with debug mode in production
from flask import Flask
app = Flask(__name__)
app.debug = True # Vulnerable: Shows debugger in production
@app.route('/user/<id>')
def get_user(id):
# When exception occurs, Flask debug page shows:
# - Full stack trace
# - Local variables in each frame
# - Source code context
# - Interactive debugger (can execute code!)
return db.query(f"SELECT * FROM users WHERE id = {id}")
if __name__ == '__main__':
app.run()
Fixed Code
<!-- Fixed: ASP.NET with custom error pages -->
<configuration>
<system.web>
<!-- Fixed: Custom errors enabled for all users -->
<customErrors mode="On" defaultRedirect="~/Error/General">
<error statusCode="404" redirect="~/Error/NotFound" />
<error statusCode="500" redirect="~/Error/ServerError" />
<error statusCode="403" redirect="~/Error/Forbidden" />
</customErrors>
</system.web>
</configuration>
<!-- Alternative: RemoteOnly shows details only to localhost -->
<customErrors mode="RemoteOnly" defaultRedirect="~/Error/General">
<!-- Developers on server see details, remote users see custom pages -->
</customErrors>
<!-- Fixed: Java web.xml with error pages -->
<web-app>
<!-- Fixed: Custom error pages for exceptions -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/views/error/general.jsp</location>
</error-page>
<!-- Fixed: Custom pages for HTTP status codes -->
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/views/error/notfound.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/views/error/servererror.jsp</location>
</error-page>
<error-page>
<error-code>403</error-code>
<location>/WEB-INF/views/error/forbidden.jsp</location>
</error-page>
</web-app>
<?php
// Fixed: PHP with proper error handling for production
// Fixed: Disable error display in production
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php/error.log');
error_reporting(E_ALL);
// Fixed: Custom error handler
set_error_handler(function($errno, $errstr, $errfile, $errline) {
// Log full details
error_log("Error [$errno]: $errstr in $errfile on line $errline");
// Show generic message to user
http_response_code(500);
include('error_pages/500.html');
exit();
});
// Fixed: Custom exception handler
set_exception_handler(function($exception) {
// Log full details internally
error_log("Exception: " . $exception->getMessage() .
"\n" . $exception->getTraceAsString());
// Show generic message to user
http_response_code(500);
include('error_pages/500.html');
exit();
});
// Fixed: Catch database errors specifically
try {
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
$result = $stmt->fetch();
} catch (PDOException $e) {
error_log("Database error: " . $e->getMessage());
http_response_code(500);
include('error_pages/database_error.html');
exit();
}
?>
// Fixed: Java servlet with proper error handling
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 userId = request.getParameter("id");
// Validate and process
if (!isValidId(userId)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
User user = userService.findById(userId);
if (user == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// Process user...
} catch (SQLException e) {
// Fixed: Log full details internally
logger.error("Database error processing request", e);
// Fixed: Generic error response
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (Exception e) {
logger.error("Unexpected error", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
# Fixed: Flask with proper error handling
from flask import Flask, render_template
import logging
app = Flask(__name__)
app.debug = False # Fixed: Debug disabled in production
# Configure logging
logging.basicConfig(
filename='/var/log/app/error.log',
level=logging.ERROR
)
# Fixed: Custom error handlers
@app.errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def internal_error(error):
# Log full details
app.logger.error(f'Server Error: {error}')
return render_template('errors/500.html'), 500
@app.errorhandler(Exception)
def handle_exception(e):
# Log full exception details
app.logger.exception('Unhandled exception')
return render_template('errors/500.html'), 500
@app.route('/user/<id>')
def get_user(id):
try:
# Use parameterized query
user = db.execute(
"SELECT * FROM users WHERE id = ?", [id]
).fetchone()
if user is None:
abort(404)
return render_template('user.html', user=user)
except Exception as e:
app.logger.error(f'Error fetching user {id}: {e}')
abort(500)
if __name__ == '__main__':
app.run()
<!-- Example custom error page (500.html) -->
<!DOCTYPE html>
<html>
<head>
<title>Server Error</title>
</head>
<body>
<h1>Something went wrong</h1>
<p>We're sorry, but an error occurred while processing your request.</p>
<p>Please try again later or contact support if the problem persists.</p>
<p>Error reference: ERR-<!-- Insert unique error ID for support lookup --></p>
<!-- No stack traces, paths, or technical details -->
</body>
</html>
CVE Examples
- CVE-2017-5638: Apache Struts error messages revealed internal details enabling attack refinement.
- CVE-2019-1003000: Jenkins default error pages exposed sensitive system information.
References
- MITRE Corporation. "CWE-756: Missing Custom Error Page." https://cwe.mitre.org/data/definitions/756.html
- OWASP Top Ten 2021: A05:2021 Security Misconfiguration.
- OWASP. "Error Handling Cheat Sheet."