CWE-248: Uncaught Exception
Overview
Uncaught exceptions cause application crashes, expose stack traces with sensitive information (file paths, internal logic, SQL queries), leave resources unclosed, abort critical operations, and enable denial of service by triggering unhandled errors.
OWASP Classification
A10:2025 - Mishandling of Exceptional Conditions
Risk
Medium: Uncaught exceptions cause application crashes (DoS), information disclosure via stack traces, resource leaks (connections, files), incomplete transactions, inconsistent state, and expose internal implementation details to attackers for reconnaissance.
Remediation Steps
Core principle: Do not allow uncaught exceptions to crash services or leak internals; handle exceptions safely and consistently.
Locate Uncaught Exceptions
When reviewing security scan results:
- Find try-catch gaps: Identify risky operations without exception handling
- Check async/promise code: Look for unhandled promise rejections
- Review file/network I/O: Find I/O operations that could throw
- Identify resource allocations: Look for connections, files not properly closed
- Check parsing operations: Find JSON/XML parsing without error handling
Vulnerable patterns:
// No try-catch around risky operation
const data = JSON.parse(userInput); // Throws on invalid JSON
// Uncaught promise rejection
fetch('/api/data').then(res => res.json()); // No .catch()
// Resource not closed on exception
const conn = await db.connect();
const result = await conn.query(sql); // Exception = conn never closed
conn.close();
Implement Global Exception Handlers (Primary Defense)
const express = require('express');
const app = express();
// VULNERABLE - uncaught exceptions crash the app
app.get('/parse', (req, res) => {
// Attack: send invalid JSON = crash entire server!
const data = JSON.parse(req.query.data);
res.json({ result: data });
});
app.get('/divide', (req, res) => {
const result = 100 / parseInt(req.query.divisor);
res.json({ result }); // Division by zero = Infinity (not exception in JS)
});
// SECURE - global error handler
// Route-level try-catch
app.get('/parse', (req, res, next) => {
try {
const data = JSON.parse(req.query.data);
res.json({ result: data });
} catch (error) {
// Pass to global error handler
next(error);
}
});
// Better - async wrapper
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/async-data', asyncHandler(async (req, res) => {
const data = await fetchExternalAPI(req.query.url);
res.json(data);
// Exceptions automatically caught and passed to error handler
}));
// Global error handler (must be last middleware)
app.use((err, req, res, next) => {
// Log detailed error server-side
console.error('Error occurred:', {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
timestamp: new Date().toISOString()
});
// Return generic error to client (no stack trace!)
res.status(err.statusCode || 500).json({
error: 'An error occurred processing your request',
requestId: req.id // For support purposes
});
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Promise Rejection:', reason);
// Don't crash - log and continue
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Graceful shutdown
process.exit(1);
});
Java/Spring:
import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
public class UserController {
// VULNERABLE - uncaught exception exposes stack trace
@GetMapping("/user/{id}")
public User getUser(@PathVariable String id) {
// Attack: id="abc" = NumberFormatException exposed to user!
return userService.findById(Integer.parseInt(id));
}
// SECURE - specific exception handling
@GetMapping("/user/{id}")
public ResponseEntity<User> getUserSafe(@PathVariable String id) {
try {
int userId = Integer.parseInt(id);
User user = userService.findById(userId);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
} catch (NumberFormatException e) {
log.warn("Invalid user ID format: {}", id);
return ResponseEntity.badRequest().build();
} catch (Exception e) {
log.error("Error retrieving user", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
// Global exception handler
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
// Log detailed error
log.error("Unhandled exception", e);
// Return generic error
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("An error occurred"));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException e) {
log.warn("Invalid argument: {}", e.getMessage());
return ResponseEntity
.badRequest()
.body(new ErrorResponse("Invalid request"));
}
}
Ensure Proper Resource Cleanup
import java.sql.*;
import java.io.*;
public class ResourceManagement {
// VULNERABLE - resources not closed on exception
public void queryDatabaseBad(String sql) throws SQLException {
Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// Process results...
// BUG: If exception occurs above, resources never closed!
rs.close();
stmt.close();
conn.close();
}
// SECURE - try-with-resources (Java 7+)
public void queryDatabaseSafe(String sql) throws SQLException {
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
// Process results...
} // Resources automatically closed even if exception occurs
}
// SECURE - manual cleanup in finally (pre-Java 7)
public void queryDatabaseManual(String sql) throws SQLException {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(DB_URL);
stmt = conn.createStatement();
rs = stmt.executeQuery(sql);
// Process results...
} finally {
// Close in reverse order, with null checks
if (rs != null) {
try { rs.close(); } catch (SQLException e) { /* log */ }
}
if (stmt != null) {
try { stmt.close(); } catch (SQLException e) { /* log */ }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { /* log */ }
}
}
}
}
Python context managers:
# VULNERABLE - file not closed on exception
def process_file_bad(filename):
f = open(filename, 'r')
data = f.read()
# Exception here = file never closed!
result = process_data(data)
f.close()
return result
# SECURE - context manager (with statement)
def process_file_safe(filename):
try:
with open(filename, 'r') as f:
data = f.read()
result = process_data(data)
return result
except FileNotFoundError:
logger.error(f"File not found: {filename}")
return None
except Exception as e:
logger.error(f"Error processing file: {e}")
return None
# File automatically closed even if exception occurs
Sanitize Error Messages for Users
from flask import Flask, jsonify
import logging
app = Flask(__name__)
# VULNERABLE - exposes stack trace to users
@app.route('/data/<int:id>')
def get_data_bad(id):
# Exception exposes full stack trace with file paths, code!
record = database.query(f"SELECT * FROM data WHERE id={id}")
return jsonify(record)
# SECURE - sanitized error messages
@app.route('/data/<int:id>')
def get_data_safe(id):
try:
# Use parameterized query
record = database.query(
"SELECT * FROM data WHERE id=?",
(id,)
)
if not record:
return jsonify({'error': 'Record not found'}), 404
return jsonify(record)
except Exception as e:
# Log detailed error server-side
logging.error(f"Error fetching data for id {id}: {str(e)}", exc_info=True)
# Return generic error to user
return jsonify({'error': 'An error occurred'}), 500
# Global error handler
@app.errorhandler(Exception)
def handle_exception(e):
# Log full details
logging.error("Unhandled exception", exc_info=True)
# Return generic response
return jsonify({'error': 'Internal server error'}), 500
# Disable debug mode in production
if __name__ == '__main__':
app.run(debug=False) # Never debug=True in production!
Handle Async/Promise Exceptions
// VULNERABLE - unhandled promise rejections
async function fetchUserDataBad(userId) {
// No try-catch = unhandled rejection if fetch fails
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user;
}
// SECURE - proper async error handling
async function fetchUserDataSafe(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error(`Failed to fetch user ${userId}:`, error);
throw new Error('Failed to fetch user data');
}
}
// Promise chains
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => processData(data))
.catch(error => {
// Handle all errors in chain
console.error('Error:', error);
displayErrorToUser('Failed to load data');
});
Verify Exception Handling
Manual verification steps
Test error responses
Verify no stack traces leak
# Send invalid requests to your API
curl -X POST http://localhost:8080/api/users -d '{"invalid": true}'
# Response should:
# - Have appropriate status code (400, 404, 500)
# - Include generic error message
# - NOT include stack traces
# - NOT include internal paths (com.example.*, /app/src/...)
Check error responses in browser DevTools
1. Submit invalid form data
2. Open DevTools (F12) -> Network tab
3. Inspect response body
4. Verify no stack traces or internal details visible
Review global exception handlers
# Find exception handlers
grep -rn "@ExceptionHandler\|@ControllerAdvice" src/
# Verify they return generic messages, not exception details
# BAD: return exception.getMessage()
# GOOD: return "An error occurred. Please contact support."
Test resource cleanup on exceptions
// Manual test: Verify resources closed even on error
try {
// Trigger an exception during database operation
service.processInvalidData("bad input");
} catch (Exception e) {
// Check logs: should see connection closed
// Check connection pool: should not leak connections
}
Check error logging
Verify full details logged server-side only
# Trigger errors and check logs
tail -f application.log
# Logs should include:
# - Full stack trace (for debugging)
# - Request details
# - User ID (if available)
# But client responses should be generic
Static analysis
Find information disclosure risks
# SonarQube rule: S5145 (Log injection)
# SpotBugs: INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE
mvn sonar:sonar
Verification checklist
- Error responses have generic messages ("Invalid request", "Internal error")
- No stack traces in HTTP responses
- No internal file paths or class names exposed
- Appropriate HTTP status codes (400, 404, 500)
- Full exception details logged server-side only
- Resources (connections, files) closed even on exception
- Global exception handler catches all unhandled exceptions
Common Vulnerable Patterns
- No try-catch around risky operations
- Catching Exception but not handling
- Not closing resources in finally/cleanup
- Exposing stack traces to users
- Empty catch blocks that swallow errors
Security Checklist
- Global exception handler implemented
- try-catch around all risky operations
- Resources cleaned up in finally/try-with-resources
- Error messages sanitized (no stack traces to users)
- Detailed errors logged server-side
- Async/promise rejections handled
- Debug mode disabled in production
- Tests verify exceptions don't crash app