Web Server Error Page Information Disclosure means your web server or application returns a default or debug error page that reveals details such as the server version, file paths or a stack trace. Fix it by switching off detailed errors in production, serving a generic custom error page, and logging the real error on the server.
What the scanner is actually detecting
Three common scanners report the same underlying problem under different names. All of them judge the server by what comes back over HTTP, not by reading your configuration.
| Scanner | ID | Finding title | Vendor rating |
|---|---|---|---|
| Nessus | 88490 | Web Server Error Page Information Disclosure | Medium |
| Tenable Web App Scanning | 98611 | Error Message | Info |
| OWASP ZAP (passive) | 90022 | Application Error Disclosure | Medium for matched error text, Low for a bare HTTP 500 |
Nessus 88490 is a remote plugin in the Web Servers family. Tenable describes it as a default error page that discloses the server version and languages used by the web server. Two details matter: its script name (pci_www_error_page_info_disclosure.nasl) marks it as a PCI-oriented check, and it requires the Settings/ParanoidReport KB item, so it only runs when the scan policy is set to show potential false alarms.
Tenable WAS 98611 flags error or warning messages in application responses. Its solution text is to disable notice, warning and error display and log those messages to a file instead.
ZAP 90022 is a passive rule that matches known error signatures, such as the file location of an unhandled exception, in pages ZAP has already seen. It skips 404 responses, and at the default threshold it also raises a Low alert for any HTTP 500 response, even one with a generic body.
Real-world risk
This is information disclosure. It does not give an attacker access on its own. What it provides is reconnaissance: exact server and framework versions to match against known vulnerabilities, absolute file paths, class and module names, and sometimes SQL fragments from failed queries. Verbose errors also make other attacks cheaper, because every failed injection attempt returns precise feedback.
One variant deserves real urgency. A Flask or Werkzeug application running with the built-in debugger enabled exposes an interactive traceback, and the Flask documentation warns that this debugger allows executing arbitrary Python code from the browser. Its PIN should not be relied on. A Django site with DEBUG = True is also more serious than a version string, since Django’s own deployment checklist notes that it leaks source code excerpts, local variables and settings. A plain version number on a 404 page is a genuine but low-impact finding.
How to confirm it on the host
From a remote machine
Start by replaying the exact URL from the scanner output, because the problem may be on one application path only. Then probe a page that does not exist and look for common leak markers:
URL=https://app.example.com
curl -sk -o probe.html -w "%{http_code}n" "$URL/no-such-page-7f3a"
grep -Eai "Apache Tomcat/|Server at|Version Information|Stack Trace|Traceback|DEBUG = True| on line |node_modules" probe.html
Any match means a remote client receives detailed output. Run the probe from a different machine than the server, since IIS and ASP.NET show details to local requests by default.
On the server
Check the configuration that controls error output for your stack:
# PHP (ini files, FPM pools, Apache overrides, application code)
grep -RniE "display_errors|log_errors" /etc/php* /etc/php-fpm.d /etc/apache2 /etc/httpd 2>/dev/null
grep -RnE "ini_set(s*['"]display_errors" /var/www 2>/dev/null
# Apache httpd signature and error documents
grep -RniE "ServerSignature|ErrorDocument" /etc/apache2 /etc/httpd 2>/dev/null
# Tomcat
grep -n "ErrorReportValve" "$CATALINA_BASE/conf/server.xml"
grep -ln "<error-page>" "$CATALINA_BASE"/webapps/*/WEB-INF/web.xml
# Django
python manage.py check --deploy
# Node.js, Flask and ASP.NET Core services under systemd
systemctl show myapp.service -p Environment
Ignore php.ini lines that start with a semicolon, since those are comments. On IIS, run these from an elevated prompt to find every location that turns protection off:
%windir%system32inetsrvappcmd.exe search config /section:system.web/customErrors /mode:Off
%windir%system32inetsrvappcmd.exe search config /section:system.webServer/httpErrors /errorMode:Detailed
How to fix it
The pattern is the same everywhere: turn off detailed output, serve a generic page, and make sure the details still reach a log.
IIS and ASP.NET
In each application’s web.config:
<configuration>
<system.web>
<customErrors mode="RemoteOnly" defaultRedirect="~/error.html" />
</system.web>
<system.webServer>
<httpErrors errorMode="DetailedLocalOnly" />
</system.webServer>
</configuration>
RemoteOnly and DetailedLocalOnly are Microsoft’s defaults: remote clients get the custom page, local requests still see details. If a reverse proxy runs on the same server, every request looks local, so use mode="On" and errorMode="Custom" instead. For ASP.NET Core, make sure ASPNETCORE_ENVIRONMENT is not Development in production and that the app calls app.UseExceptionHandler("/Error") outside Development, as Microsoft’s templates do.
Apache httpd and PHP
Set these in the php.ini used by the web SAPI (on Debian and Ubuntu that is /etc/php/<version>/fpm/php.ini or /etc/php/<version>/apache2/php.ini, not the CLI file):
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/php_errors.log
The log file must be writable by the web server user. Because display_errors can be changed at runtime with ini_set(), lock it where you can. In a PHP-FPM pool file:
php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
With mod_php, put php_admin_flag display_errors off in the virtual host. Then remove the version footer from Apache’s own error pages and point them at static files:
ServerSignature Off
ErrorDocument 404 /errors/404.html
ErrorDocument 500 /errors/500.html
sudo apachectl configtest && sudo systemctl reload apache2 # httpd on RHEL
sudo systemctl restart php8.3-fpm # match your FPM unit name
If nginx sits in front, add server_tokens off; and error_page directives there too, since nginx prints its version on its own error pages by default.
Tomcat and Java
Tomcat’s default ErrorReportValve shows the Tomcat version and can show stack traces. Configure it explicitly inside the <Host> element of conf/server.xml:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.ErrorReportValve"
showReport="false" showServerInfo="false" />
</Host>
Add application error pages in each WEB-INF/web.xml so users see something friendlier than a bare status code:
<error-page>
<error-code>404</error-code>
<location>/errors/404.html</location>
</error-page>
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/errors/500.html</location>
</error-page>
Restart Tomcat afterwards.
Node.js and Express
Express’s built-in error handler writes err.stack to the client unless NODE_ENV is production. Set it in the service unit (Environment=NODE_ENV=production) and add a final error handler that logs and returns a generic message:
app.use((err, req, res, next) => {
if (res.headersSent) return next(err);
console.error(err);
res.status(500).send('Internal Server Error');
});
Also search the code for handlers that send err.stack or err.message straight to the response.
Django and Flask
# Django settings.py (production)
DEBUG = False
ALLOWED_HOSTS = ["app.example.com"]
Add 404.html and 500.html templates to your root template directory, then run python manage.py check --deploy. For Flask, run the app under a production WSGI server, never the development server, and remove --debug, FLASK_DEBUG and app.run(debug=True) from anything that starts production.
How to verify the fix and rescan
- Repeat the remote curl probes, including the exact URL from the finding. None of the markers should appear.
- Confirm the details now land in the log you configured, by triggering a known error in staging.
- Rescan Nessus with the same policy, including the paranoid “show potential false alarms” setting. Without it, plugin 88490 never runs, so its absence proves nothing.
- For Tenable WAS or ZAP, rescan or re-crawl the affected URLs. ZAP 90022 is passive, so it only re-evaluates pages that pass through ZAP again.
What can break and how to roll back
- Developers lose browser tracebacks. Confirm logging works before the change, or you trade an information leak for an outage nobody can diagnose.
- Django returns 400 for everything. With
DEBUG = False, a missing or wrongALLOWED_HOSTSmakes Django answer every request with Bad Request (400). - Empty Tomcat error bodies. With both valve flags false, Tomcat returns only the status code where no application error page exists. Monitoring that matches error page text may need updating.
- Side effects of NODE_ENV. Express and any library that reads
NODE_ENVmay behave differently in production mode, so test the service in staging first. - Broken error pages. A dynamic error page can fail itself. Static HTML is the safest target.
To roll back, copy each file before editing (cp -a php.ini php.ini.bak, likewise for server.xml, web.config and settings files), then restore the copy and reload or restart the service. On IIS, appcmd add backup BeforeErrorFix and appcmd restore backup BeforeErrorFix cover server-level configuration.
Common false positive reasons
- Paranoid-mode results. Nessus 88490 runs only with paranoid reporting, Tenable’s signal that it may include potential false alarms. Replay the request before assigning work.
- Error text that is content. ZAP’s documentation notes the alert can be a false positive when the error message appears inside a documentation page, such as a knowledge base article about exceptions.
- Generic 500 responses. ZAP’s Low alert fires on any HTTP 500. If the body is generic, nothing is disclosed; the 500 itself is a separate bug.
- Wrong site or local scan. A scan by IP can reach a different virtual host than the one you fixed, and a scanner on the server itself sees IIS and ASP.NET local-only details by design.
- Stale results from a scan that predates the change.
An upstream WAF that hides errors from internet users does not make the finding false if the scanner reached the origin directly. For a broader checklist, see our guide to the common causes of vulnerability scanner false positives.
FAQ
Is a server version on a 404 page really a vulnerability?
It is low-impact information disclosure. It helps attackers pick exploits but grants nothing by itself. Stack traces and debug pages are more serious, and an exposed interactive debugger needs fixing immediately.
Does hiding the version fix an outdated server?
No. It removes a hint, not the vulnerable code. A credentialed scan still reads the installed version locally, so keep patching.
Will display_errors = Off hide errors from my team?
Only from browsers. With log_errors = On the same messages go to the configured error log, which PHP’s manual recommends for production sites.
Why does Nessus 88490 not appear in every scan?
The plugin requires paranoid reporting. Scans whose policy does not show potential false alarms never run it.
Tracking this finding across many hosts
This finding tends to reappear on every new application and virtual host, so tracking it matters as much as fixing it. SITEY is one option: it imports findings from 16 scanners, including Nessus results uploaded as .nessus exports, and merges duplicates per scanner (not across scanners, so a Nessus 88490 and a ZAP 90022 on the same page stay separate). Its AI triage suggests likely false positives with evidence for a human to decide, and per-finding retest is available for Nessus, Acunetix and Burp findings.