Information disclosure through error messages occurs when applications expose sensitive information in error pages, logs, debug output, or exception messages. This includes:
Error messages are meant to help developers debug, but in production they become a roadmap for attackers. Stack traces, database queries, file paths, and configuration details reveal the application's architecture and vulnerabilitis.
Error: You have an error in your SQL syntax; check the manual that corresponds
to your MySQL server version for the right syntax to use near
"'test''" at line 1
SQL Query: SELECT * FROM products WHERE name LIKE '%test'%' AND active = 1
What the attacker learns:
Database is MySQL (version from error)
Exact SQL query structure: SELECT * FROM products WHERE name LIKE '%[INPUT]%' AND active = 1
SQL injection is possible (query error shows uncaught exception)
There's an active field (logic discovery)
Table name and column names (products, name, active)
The attack:
With this information, attacker crafts SQL injection:
Result:
Complete SQL injection exploitation
Database compromise
Data theft or destruction
The fix:
Never show SQL errors to users:
Finding it: Submit invalid input (single quotes, special characters). Check error messages for SQL syntax errors, table/column names, query structure.
Exploit:
Scenario 2: Stack Trace Revealing File Paths and Code
Application has an unhandled exception:
The verbose error response:
What the attacker learns:
Web root is /var/www/html (server architecture)
File structure:
/api/user.php - handles user API
/src/User/Manager.php - user management logic
/api/router.php - routing logic
Code flow: How requests are routed and processed
Function names: handleRequest(), getUserProfile() (API endpoints)
PHP version and framework (error format)
Exact line numbers where errors occur
The attack:
With this knowledge, attacker:
Enumerates file structure
Tests each API endpoint knowing exact code paths
Identifies vulnerable functions
Finds bypass opportunities in error handling
Result:
Complete application architecture exposed
Targeted exploitation possible
Privilege escalation paths identified
The fix:
Catch all exceptions and return generic errors:
Finding it: Trigger exceptions with invalid input. Check error messages for stack traces, file paths, function names.
Scenario 3: Debugging Code Left in Production
Application has debugging statements that reveal sensitive data:
What's leaked:
Emails - User enumeration (who has accounts)
Password hashes - Can be cracked offline
API keys - Direct service access
Database credentials - Database compromise
Full user objects - All user data exposed
The attack:
Attacker:
Captures API response with full user object
Gets user's API key
Uses API key to access other services
Accesses password hash, runs offline crack
Logs in with real password
Result:
Complete user account compromise
Access to all services
Database and API compromise
The fix:
Remove all debugging code in production:
Finding it: Check response bodies for unnecessary data. Search code for console.log(), println(), print_r(). Review debug variables in output.
Scenario 4: Version Information in Error Pages
Application exposes framework and library versions:
What the attacker learns:
Apache 2.4.41 - Version with known CVEs
PHP 7.4.3 - Version with known vulnerabilities
Laravel 8.12.0 - Framework version with known exploits
Application structure - Uses Models (Laravel MVC)
The attack:
Attacker:
Looks up CVE-2020-xxxx for Apache 2.4.41
Finds public exploit for PHP 7.4.3
Searches Exploit-DB for Laravel 8.12.0
Finds remote code execution vulnerability
Exploits application directly
Result:
Remote code execution
Server compromise
Application takeover
The fix:
Hide version information:
Finding it: Check HTTP headers. Look at error pages. Search for version strings in responses.
Exploit:
Scenario 5: No Custom Error Page (Information Leakage)
Application displays default server error pages:
What the attacker learns:
Exact server configuration
Installed modules
File paths
Known vulnerabilities for that configuration
The fix:
Create custom error pages:
Configure in web server:
Finding it: Trigger errors and check if default error pages shown instead of custom pages.
Scenario 6: Sensitive Information in Debug Comments
Developer leaves credentials or sensitive info in comments:
What the attacker learns:
Debug credentials: admin / AdminDebug123!
Database credentials: User and password
Database server: db.internal.company.com
Database name: prod_db
Port: 5432
The attack:
Attacker:
Uses debug credentials to log in as admin
Or uses database credentials to connect directly to PostgreSQL
Dumps entire database
Complete compromise
Result:
Admin access
Database access
Complete data breach
The fix:
Never leave credentials in comments:
Finding it: Search source code for comments containing: password, key, secret, TODO, FIXME, DEBUG, HACK.
Exploit:
How to Identify Information Disclosure During Testing
/search?query=test' OR '1'='1
/search?query=test' UNION SELECT username, password FROM users--
/search?query=test'; DROP TABLE products;--
<?php
try {
$result = $pdo->query("SELECT * FROM products WHERE name LIKE ?", [$query]);
} catch (PDOException $e) {
// Log error server-side
error_log($e->getMessage());
// Show generic message to user
http_response_code(500);
echo "An error occurred. Please try again.";
}
?>
# Trigger error with SQL injection payload
curl "http://target.com/search?query=test'"
# If error shows SQL query, vulnerability confirmed
POST /api/user/profile
Content-Type: application/json
{"user_id": "abc"} // Invalid type
Fatal error: Uncaught TypeError: Argument 1 passed to getUserProfile()
must be of the type int, string given, called in
/var/www/html/api/user.php on line 42 and defined in
/var/www/html/src/User/Manager.php:28 Stack trace:
#0 /var/www/html/api/user.php(42): getUserProfile('abc')
#1 /var/www/html/api/router.php(156): User\API->handleRequest()
#2 /var/www/html/index.php(12): Router->dispatch()
#3 {main}
thrown in /var/www/html/src/User/Manager.php on line 28
<?php
try {
$user = getUserProfile($_POST['user_id']);
echo json_encode($user);
} catch (Exception $e) {
// Log error with full details
error_log($e->getMessage() . "\n" . $e->getTraceAsString());
// Return generic error to user
http_response_code(400);
echo json_encode(['error' => 'Invalid request']);
}
?>
// Debugging code left in production
app.get('/api/login', (req, res) => {
const user = authenticateUser(req.body.email, req.body.password);
// DEBUG: Console logging (visible in server logs)
console.log(`Login attempt for: ${req.body.email}`);
console.log(`Password hash: ${user.password_hash}`);
console.log(`API Key: ${user.api_key}`);
console.log(`Database connection: ${process.env.DB_URL}`);
if (user) {
res.json({user: user}); // DEBUG: Returns full user object
}
});
app.get('/api/login', (req, res) => {
const user = authenticateUser(req.body.email, req.body.password);
if (user) {
// Return only necessary fields
res.json({
token: generateToken(user.id),
name: user.name
// Don't return: password_hash, api_key, etc.
});
} else {
res.status(401).json({error: 'Invalid credentials'});
}
});
HTTP/1.1 500 Internal Server Error
Server: Apache/2.4.41 (Ubuntu)
X-Powered-By: PHP/7.4.3
X-AspNet-Version: 4.0.30319
Content-Type: text/html
Laravel Framework 8.12.0 Error:
Class 'App\Models\InvalidClass' not found
# Apache httpd.conf
ServerTokens Prod
ServerSignature Off
# PHP php.ini
expose_php = Off
# Custom error pages (don't show version)
<ErrorDocument 500 /errors/500.html>
# Check headers for version info
curl -I http://target.com
# Check error pages
curl http://target.com/nonexistent.php
# Look for version strings in HTML
curl http://target.com | grep -i "version\|powered"
IIS default error page shows:
- IIS version
- .NET Framework version
- Module that caused error
- Source file path
- Error code and description
<!-- /errors/500.html -->
<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body>
<h1>Something Went Wrong</h1>
<p>We're sorry, an error occurred. Our team has been notified.</p>
<p>Error reference: [UNIQUE_ID]</p>
</body>
</html>
def authenticate():
# DEBUG: Test account for debugging
# Username: admin, Password: AdminDebug123!
if username == 'admin' and password == 'AdminDebug123!':
return True
# DB connection string (left from debugging)
# postgresql://user:SecurePass456@db.internal.company.com:5432/prod_db
return check_database(username, password)
def authenticate():
# Authenticate user against database
return check_database(username, password)
# Use environment variables for sensitive data
DB_URL = os.getenv('DATABASE_URL')
# Search source code in git
git log -p | grep -i "password\|secret\|key"
# Or search current code
grep -r "password\|TODO\|FIXME\|DEBUG" src/
# Search for debug code
grep -r "console.log\|println\|print_r\|var_dump" src/
# Search for credentials in comments
grep -r "password\|api_key\|secret\|TODO" src/
# Check for sensitive data in responses
grep -r "password\|credit_card\|ssn" src/
<!-- Generic, no technical details -->
<h1>Error</h1>
<p>Something went wrong. Please try again.</p>
try:
# Code
except Exception as e:
# Log everything
logger.error(f"Error: {e}\n{traceback.format_exc()}")
# Show generic message
return "An error occurred"
if DEBUG: # DEBUG only enabled locally
print(sensitive_data)
ServerTokens Prod
ServerSignature Off
expose_php = Off
except SQLError as e:
# Don't show: Error in query "SELECT * FROM users..."
# Show: "Database error occurred"
return "A database error occurred"
# BAD
logger.info(f"Login: {username}, password: {password}")
# GOOD
logger.info(f"Login attempt for: {username}")