This vulnerability occurs when an application reveals sensitive information through its error messages. This can include detailed stack traces, database error messages, internal file paths, framework versions, private IP addresses, or configuration details. Attackers deliberately trigger errors to gather this information, which helps them understand the application’s technology stack, structure, and potential weaknesses. 🕵️♂️📜
This is primarily a configuration issue. Most modern web frameworks have a “debug” or “development” mode that intentionally shows detailed errors, and a “production” mode that shows generic error pages. The vulnerability is almost always caused by deploying to production without disabling debug mode.The fix involves:
Disabling Debug Mode: Ensure the framework’s production setting is active.
Custom Error Pages: Configure user-friendly, generic error pages (e.g., for 404 Not Found, 500 Internal Server Error) that do not reveal internal details.
Logging: Log detailed errors server-side for developers, but do not display them to users.
Vulnerable Scenario 2: Flask Debug Mode in Production
Running Flask with debug mode enabled via environment variable or code.
# DANGEROUS: Running Flask in debug mode on production serverexport FLASK_ENV=developmentexport FLASK_DEBUG=1flask run --host=0.0.0.0
# app.py (Alternative dangerous way)if __name__ == '__main__': # DANGEROUS: Never run with debug=True in production. app.run(debug=True, host='0.0.0.0')
Django: Set DEBUG = False in your production settings.py. Configure ADMINS and LOGGING to receive error details server-side. Create custom HTML templates for error views (e.g., templates/500.html, templates/404.html).
Flask: Ensure FLASK_ENV is set to production and FLASK_DEBUG is 0 (or unset) in production. Use Flask’s @app.errorhandler() decorator or app.register_error_handler() to define custom, generic error pages. Configure logging.
# settings.py (Django Production - Secure)DEBUG = False # SECUREALLOWED_HOSTS = ['yourdomain.com', '[www.yourdomain.com](https://www.yourdomain.com)']# Configure logging to capture details server-sideLOGGING = { ... }ADMINS = [('Admin', 'admin@yourdomain.com')] # For error emails
# app.py (Flask Production - Secure)from flask import Flask, render_templateapp = Flask(__name__)# Load production config where DEBUG=Falseapp.config.from_object('config.ProductionConfig')@app.errorhandler(500)def internal_server_error(e): # SECURE: Log the detailed error server-side app.logger.error(f'Server Error: {e}', exc_info=True) # SECURE: Show a generic error page to the user return render_template('500.html'), 500@app.errorhandler(404)def page_not_found(e): return render_template('404.html'), 404# Run via WSGI server (like Gunicorn/uWSGI), not app.run() in production
Intentionally trigger errors in the deployed application (e.g., browse to non-existent pages for 404, cause exceptions for 500). Verify that generic, user-friendly error pages are shown, and no stack traces, configuration details, or internal paths are visible in the browser. Check server logs to ensure detailed errors are being recorded there.
While less sensitive than a full stack trace, the default Spring Boot “Whitelabel Error Page” still reveals that the application uses Spring Boot and potentially exposes some context path information.
# application.properties (Production)# DANGEROUS: Shows the default Spring error page.server.error.whitelabel.enabled=trueserver.error.include-stacktrace=never # Stack trace is off, but whitelabel reveals framework info
Set server.error.include-stacktrace=never (or on_param for controlled debugging). Set server.error.whitelabel.enabled=false. Create custom error views (e.g., map error paths in @ControllerAdvice or provide static error/404.html, error/500.html pages). Configure server-side logging.
# application.properties (Production - Secure)# SECURE: Never include stack traces in responses.server.error.include-stacktrace=never# SECURE: Disable the default Spring error page.server.error.whitelabel.enabled=false
// GlobalExceptionHandler.java (Example using @ControllerAdvice)import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;@ControllerAdvicepublic class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(Exception.class) // Catch general exceptions public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { // SECURE: Log the detailed error server-side logger.error("Unhandled exception for request: {}", req.getRequestURL(), e); // SECURE: Show a generic error view ModelAndView mav = new ModelAndView(); mav.addObject("exception", null); // Don't pass exception details mav.addObject("url", req.getRequestURL()); mav.setViewName("error/generic"); // Path to your generic error template return mav; }}
Trigger 404 and 500 errors. Verify that custom, generic error pages are displayed. Inspect the HTML source and network response to ensure no stack traces or sensitive framework details are present. Check server logs for detailed error information.
Vulnerable Scenario 1: Developer Exception Page in Production
The check for env.IsDevelopment() is flawed or missing, causing the detailed developer page to be used in production.
// Startup.cs - Configure()public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ // DANGEROUS: This logic might incorrectly evaluate to true in production, // or the UseDeveloperExceptionPage() call might be outside the 'if'. // Always ensure this ONLY runs in Development. if (env.IsDevelopment() || env.IsStaging()) // Staging might also be risky { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see [https://aka.ms/aspnetcore-hsts](https://aka.ms/aspnetcore-hsts). app.UseHsts(); } // If UseDeveloperExceptionPage() was outside the 'if', it would run always! // app.UseDeveloperExceptionPage(); // DANGEROUS if placed here // ...}
@model ErrorViewModel<h2>Error Details (DO NOT SHOW IN PRODUCTION):</h2><p>@Model.ExceptionDetails?.Message</p><pre>@Model.ExceptionDetails?.StackTrace</pre> ```#### Mitigation and Best PracticesStrictly ensure `app.UseDeveloperExceptionPage()` is only called when `env.IsDevelopment()`. Use `app.UseExceptionHandler("/Path/To/Generic/Error")` for all other environments. Ensure your custom error handler action logs details server-side but passes only minimal, non-sensitive information (or none) to the view model rendered for the user.#### Secure Code Example```csharp// Startup.cs - Configure() (Secure)public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { // SECURE: Use a generic handler for non-development environments. app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); // ...}// Controllers/HomeController.cs (Secure Error Action)public IActionResult Error(){ var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>(); // SECURE: Log the detailed error server-side _logger.LogError(exceptionHandlerPathFeature?.Error, "Unhandled error for RequestId {RequestId}", Activity.Current?.Id ?? HttpContext.TraceIdentifier); // SECURE: Pass only necessary, non-sensitive info (like RequestId for support) return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier // DO NOT pass exception details });}
@model ErrorViewModel@if (!string.IsNullOrEmpty(Model.RequestId)){ <p><strong>Request ID:</strong> <code>@Model.RequestId</code></p> <p>If you contact support, please provide this ID.</p>}
Trigger errors in a production-like environment. Verify the generic error page defined in UseExceptionHandler is shown. Ensure no stack traces or sensitive appsettings.json values are displayed. Check server logs for detailed error information.
Vulnerable Scenario 2: display_errors = On in php.ini
Even if APP_DEBUG=false, PHP itself might be configured to display errors directly in the output.
; php.ini (Production Server); DANGEROUS: Errors, including paths and potentially sensitive info,; will be rendered directly in the HTML output.display_errors = Onerror_reporting = E_ALLlog_errors = On ; Logging is good, but display_errors should be Offerror_log = /var/log/php_errors.log
Laravel: Set APP_DEBUG=false in your production .env file. Configure Laravel’s logging channels in config/logging.php to capture errors server-side. Create custom Blade views for error pages (e.g., resources/views/errors/500.blade.php).
General PHP: In php.ini for production, set display_errors = Off and log_errors = On. Configure error_reporting appropriately (e.g., E_ALL & ~E_DEPRECATED & ~E_STRICT) and set error_log to a secure file path. Use set_exception_handler() to define a custom handler that logs details and shows a generic page.
# .env (Laravel Production - Secure)APP_ENV=productionAPP_DEBUG=false # SECUREAPP_URL=[https://myapp.com](https://myapp.com)LOG_CHANNEL=stack # Or configure specific channels like Slack, file etc.
Trigger PHP errors (e.g., divide by zero, call undefined function) and framework exceptions in a production environment. Verify that no PHP error messages, stack traces, file paths, or framework debug screens (like Ignition) are shown to the browser. Check the configured error_log file or Laravel logs (storage/logs/laravel.log) for detailed error information.
Typically handled by Express error-handling middleware. The vulnerability is sending the full err object (including err.stack) to the client in production.
If no error handling middleware catches an error, Node might crash or default handlers might leak stack traces to the console/output depending on the environment.
Implement a robust Express error-handling middleware (a function with (err, req, res, next) arguments, placed after all routes). Inside this middleware, check process.env.NODE_ENV. If it’s 'production', log the full err object server-side but send only a generic error message/object to the client.
Trigger errors in a production-like environment (NODE_ENV=production). Verify that only generic JSON messages or HTML pages are returned. Ensure no stack traces or internal object details are present in the response body. Check server logs (console or log files) for detailed error information.
Vulnerable Scenario 2: Custom Error Page Leaking Data
A custom error page (e.g., public/500.html or a view rendered by an ExceptionApp) might inadvertently include sensitive instance variables passed from the controller or environment details.
# app/controllers/concerns/exception_handler.rb (Example custom handler)module ExceptionHandler extend ActiveSupport::Concern included do rescue_from StandardError do |exception| # SECURE: Log details Rails.logger.error exception.message Rails.logger.error exception.backtrace.join("\n") # DANGEROUS: Passing the exception object to the view render file: Rails.root.join('public', '500.html'), status: 500, locals: { exception: exception } end endend# If public/500.html or the rendered view then displays exception.message or similar...
Ensure config.consider_all_requests_local = false in config/environments/production.rb. Customize static error pages (public/404.html, public/500.html, etc.) or use a custom config.exceptions_app that renders generic views without passing raw exception details. Configure logging for server-side details.
Trigger 404 and 500 errors in a production environment. Verify that the static public/404.html / public/500.html pages (or your custom generic error views) are displayed. Inspect the response source code for any stack traces, code snippets, or configuration details. Check log/production.log for detailed error information.