Information disclosure is often dismissed as a low-severity issue — "it's just version numbers, attackers already know what software you're running." This framing misunderstands how targeted attacks actually work. Attackers do not know in advance what software you are running. Version headers, error messages, and debug endpoints are exactly how they find out — turning a generic scan into a targeted attack against your specific configuration.
Server Version Headers
By default, most web servers announce themselves in detail:
Server: Apache/2.4.51 (Ubuntu)
X-Powered-By: PHP/8.0.13
X-AspNet-Version: 4.0.30319
An attacker with this information goes directly to CVE databases and searches for vulnerabilities in Apache 2.4.51, PHP 8.0.13, and ASP.NET 4.0.30319. If any of these versions have known unpatched CVEs, the attack path is immediately clear. Without this information, the attacker must probe blindly — which takes longer and generates more detectable noise.
Fix (Nginx):
server_tokens off;
Fix (Apache):
ServerTokens Prod
ServerSignature Off
Fix (PHP):
; php.ini
expose_php = Off
Stack Traces in Error Responses
Development frameworks often display detailed error information when exceptions occur. This is invaluable for debugging but catastrophic in production:
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: user_profiles
[SQL: SELECT user_profiles.id AS user_profiles_id FROM user_profiles WHERE user_profiles.user_id = ?]
[parameters: (42,)]
File "/app/routes/profile.py", line 83, in get_profile
result = db.execute(query).fetchone()
This single error message reveals the database engine (SQLite), table names, column names, query structure, and exact file paths and line numbers in the source code. A SQL injection attempt just became dramatically easier.
Fix: Ensure production environments never display stack traces to users. Log them server-side, show users a generic error page.
# Flask
app.config['PROPAGATE_EXCEPTIONS'] = False
app.config['TESTING'] = False
@app.errorhandler(500)
def internal_error(e):
app.logger.exception("Unhandled exception")
return render_template('500.html'), 500
Debug Endpoints and Admin Panels
Development frameworks often include debug interfaces that are accidentally left enabled in production:
- Django Debug Toolbar and
DEBUG=Truemode - Flask debugger (interactive Python shell accessible from the browser)
- Laravel
APP_DEBUG=true - Spring Boot Actuator endpoints (
/actuator/env,/actuator/heapdump) - Swagger/OpenAPI documentation exposed in production
- phpMyAdmin, Adminer, or similar database admin tools on public paths
The Flask debugger deserves special mention: when enabled in production, it provides an interactive Python shell in the browser, reachable without authentication. This is effectively unauthenticated remote code execution. Always set FLASK_DEBUG=0 and FLASK_ENV=production in production.
Backup Files and Source Code Exposure
Web servers sometimes accidentally serve files that should not be accessible:
/.git/config → Git configuration, remote URL, sometimes credentials
/config.php.bak → PHP configuration backup with database credentials
/wp-config.php → WordPress database credentials
/.env → Environment file with API keys and secrets
/web.config.bak → IIS configuration backup
/database.sql → Database dump
Automated scanners probe for hundreds of these paths. An accidentally exposed .env file can expose database credentials, API keys, and application secrets in a single request.
Fix: Explicitly deny access to sensitive file extensions in your web server configuration:
# Nginx — block common sensitive files
location ~* [.](env|git|sql|bak|config|log|ini)$ {
deny all;
return 404;
}
location ~ /[.] {
deny all; # Block all dotfiles
}
Directory Listing
When no index.html or index.php exists in a directory and directory listing is enabled, the web server renders the full file tree. This exposes upload directories, log files, and any other files that happen to be in the directory.
# Nginx
autoindex off;
# Apache
Options -Indexes
Verbose HTTP Response Headers
Beyond server version, other response headers can reveal infrastructure details:
X-Generator: Drupal 9
X-Drupal-Cache: MISS
X-Varnish: 12345 67890
Via: 1.1 varnish (Varnish/6.2)
Each of these narrows down the attack surface. Remove or sanitize headers that reveal CMS versions, caching infrastructure details, or internal system names.
The Principle
The goal is not perfect secrecy about your technology stack — a determined attacker can often fingerprint your stack through behavioral analysis regardless. The goal is to slow down and complicate reconnaissance, to avoid making it trivially easy, and to eliminate the cases where a single error message hands an attacker all the information they need to exploit you. Defense in depth starts with not handing attackers a roadmap.