CWE-215: Insertion of Sensitive Information Into Debugging Code
Overview
Insertion of sensitive information into debugging code occurs when debug statements, verbose logging, stack traces, or development features expose passwords, tokens, internal paths, SQL queries, or system architecture in production environments, enabling information disclosure and attack reconnaissance.
OWASP Classification
A10:2025 - Mishandling of Exceptional Conditions
Risk
Medium: Debug code in production exposes sensitive data (credentials, tokens), internal system details (file paths, versions, configurations), business logic, SQL queries, stack traces, and API keys. Attackers use this information to map systems, find vulnerabilities, and extract credentials.
Remediation Steps
Core principle: Never expose diagnostic or debugging instrumentation to untrusted clients; runtime responses must be constructed independently of any debug or developer-only state.
Identify Debug Code in Production
When reviewing security scan results, look for:
- Debug mode flags: Configuration settings that enable verbose error output
- Debug logging: Log statements that record sensitive data like passwords, tokens, or API keys
- Debug endpoints: Routes that expose configuration, environment variables, or system information
- Error handlers: Exception handling that reveals stack traces or internal details to users
- Development artifacts: Commented or conditional debug code that might execute in production
Disable Debug Mode in Production
Ensure debug mode is controlled by environment variables and defaults to disabled:
- Set debug flags to
Falsein production configuration - Use environment-based configuration classes (Development vs Production)
- Never hardcode
DEBUG=Trueor equivalent settings - Disable verbose error output (stack traces, detailed messages)
- Use production-grade web servers, not development servers
Remove Debug Endpoints and Logging
Eliminate debug code that could leak information:
- Remove or conditionally disable debug routes (
/debug/*,/test/*,/admin/debug) - Replace console logging with structured logging frameworks
- Never log sensitive data (passwords, tokens, credit cards, API keys)
- Remove debug endpoints entirely or protect them with environment checks
- Use appropriate log levels (INFO/WARNING in production, not DEBUG)
Implement Generic Error Handling
Prevent stack traces and internal details from reaching users:
- Configure framework to never include stack traces in responses
- Implement global exception handlers that return generic error messages
- Log detailed errors server-side with full context and stack traces
- Return user-friendly error messages ("Internal server error", "An error occurred")
- Never expose database queries, file paths, or internal system details in errors
Configure Secure Logging
Implement logging that protects sensitive information:
- Use structured logging frameworks (Winston, SLF4J, Python logging) instead of console output
- Set appropriate log levels by environment (INFO/WARNING in production, DEBUG in development)
- Implement log filters to redact sensitive data (passwords, tokens, credit cards)
- Log only non-sensitive identifiers (usernames, user IDs, request IDs)
- Store logs securely with appropriate access controls
Verify Debug Code Removal
Test that debug features are properly disabled in production:
- Verify debug mode flags are
Falsein production configuration - Confirm debug endpoints return 404 or are completely absent
- Test that errors return generic messages without stack traces
- Review logs to ensure no sensitive data is recorded
- Validate that production log levels are INFO or WARNING (not DEBUG)
Common Vulnerable Patterns
Debug Mode Enabled
# Flask/Django
app.config['DEBUG'] = True # Shows stack traces to users!
DEBUG = True # In settings.py
// Node.js
app.listen(3000, () => {
console.log('Server running');
}); // No NODE_ENV check, defaults to development
Why this is vulnerable: Debug mode exposes full stack traces with file paths, code snippets, library versions, and internal variables when errors occur. Attackers use this information to map application structure and identify vulnerabilities.
Logging Sensitive Data
print(f'Login: {username} / {password}') # Password in logs!
logger.debug(f'Auth token: {auth_token}')
logger.info(f'API key: {api_key}')
console.log('User credentials:', username, password); // Exposed in logs
console.log('Request:', req.headers); // May contain authorization tokens
logger.debug("Password hash: " + passwordHash); // Sensitive crypto info
System.out.println("Credit card: " + ccNumber); // PCI violation
Why this is vulnerable: Log files are accessible to operations teams, backup systems, and log aggregation services. Passwords, tokens, and keys in logs can be extracted by attackers who gain access to these systems.
Debug Endpoints Exposed
@app.route('/debug/config')
def debug_config():
return jsonify(app.config) # Exposes SECRET_KEY, DATABASE_URL!
@app.route('/debug/env')
def debug_env():
return jsonify(dict(os.environ)) # All environment variables!
app.get('/debug/routes', (req, res) => {
res.json(app._router.stack); // Exposes all routes
});
app.get('/admin/debug', (req, res) => {
res.json({ config: config, env: process.env }); // Full config dump
});
Why this is vulnerable: Debug endpoints are often forgotten in production deployments. They expose configuration, database credentials, API keys, internal routes, and system architecture to anyone who discovers them.
Verbose Error Messages
except Exception as e:
return str(e), 500 # Full exception message to user!
# May include: SQL queries, file paths, internal logic
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message, // Internal error details
stack: err.stack // Full stack trace!
});
});
catch (SQLException e) {
return ResponseEntity.status(500)
.body(e.getMessage()); // Exposes SQL query structure
}
Why this is vulnerable: Error messages reveal SQL query structure, file system paths, internal business logic, and technology stack. Attackers use this reconnaissance to craft targeted attacks.
Console Logging in Production
console.log('Processing payment for:', userId, amount);
console.log('Database query:', sqlQuery);
console.error('Auth failed:', username, 'with token:', token);
print(f'User {user_id} accessed {resource}') # Debug statement left in
print(f'SQL: {query}') # Exposes query structure
Why this is vulnerable: Console output in production servers is logged to files. These files accumulate sensitive data over time and are often accessible through log aggregation systems or server access.
Secure Patterns
Secure Pattern: Environment-Based Debug Configuration (Python/Flask)
import logging
import os
from flask import Flask, jsonify, request
app = Flask(__name__)
# Correct: debug disabled in production
app.config['DEBUG'] = os.getenv('DEBUG', 'False') == 'True'
# Configure logging properly
log_level = logging.DEBUG if app.debug else logging.INFO
logging.basicConfig(level=log_level)
logger = logging.getLogger(__name__)
@app.route('/login', methods=['POST'])
def login_safe():
username = request.form['username']
password = request.form['password']
# Correct: log username only, not password
logger.info(f'Login attempt for user: {username}')
user = authenticate(username, password)
if not user:
# Generic message to user
logger.warning(f'Failed login for: {username}')
return jsonify({'error': 'Invalid credentials'}), 401
logger.info(f'Successful login: {username}')
return jsonify({'token': create_token(user)})
# Only enable debug routes in development
if app.debug:
@app.route('/debug/config')
def debug_config():
# Even in debug, filter sensitive keys
safe_config = {k: v for k, v in app.config.items()
if k not in ['SECRET_KEY', 'DATABASE_URL']}
return jsonify(safe_config)
@app.errorhandler(Exception)
def handle_error(error):
# Log detailed error server-side
logger.error(f'Request failed: {error}', exc_info=True)
# Generic message to user (never stack trace)
if app.debug:
# Only in development
return jsonify({'error': str(error)}), 500
else:
return jsonify({'error': 'Internal server error'}), 500
if __name__ == '__main__':
# Production: debug=False, use proper WSGI server
app.run(debug=app.debug)
Why this works:
- Debug mode controlled by environment variable, defaults to False
- Logs username only, never passwords or sensitive data
- Debug endpoints only exist in development mode
- Generic error messages to users, detailed logging server-side
- Proper log levels by environment (DEBUG in dev, INFO in production)
Secure Pattern: Disabled Error Display (PHP)
<?php
// Correct: disable errors in production
if (getenv('APP_ENV') === 'production') {
ini_set('display_errors', '0');
error_reporting(0);
} else {
ini_set('display_errors', '1');
error_reporting(E_ALL);
}
function login($username, $password) {
// Correct: log to file, not screen
error_log("Login attempt for: $username");
// NEVER log password
$user = authenticate($username, $password);
if (!$user) {
error_log("Failed login: $username");
// Generic error to user
http_response_code(401);
echo json_encode(['error' => 'Invalid credentials']);
return;
}
error_log("Successful login: $username");
echo json_encode(['token' => createToken($user)]);
}
// Custom error handler - don't expose details
set_error_handler(function($errno, $errstr, $errfile, $errline) {
// Log server-side
error_log("Error: $errstr in $errfile:$errline");
// Generic message to user
if (getenv('APP_ENV') !== 'production') {
echo "Error: $errstr\n";
} else {
http_response_code(500);
echo json_encode(['error' => 'Internal server error']);
}
return true;
});
Why this works:
- Error display disabled in production (display_errors=0)
- Errors logged to files, not shown to users
- Custom error handler prevents stack trace exposure
- Never logs passwords or sensitive data
Secure Pattern: Production Logging Configuration (Node.js/Express)
const express = require('express');
const winston = require('winston');
const app = express();
// Configure logging based on environment
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Only console in development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Correct: log username, NEVER password
logger.info('Login attempt', { username });
const user = authenticate(username, password);
if (!user) {
logger.warn('Failed login', { username });
return res.status(401).json({ error: 'Invalid credentials' });
}
logger.info('Successful login', { username });
res.json({ token: createToken(user) });
});
// Debug routes only in development
if (process.env.NODE_ENV !== 'production') {
app.get('/debug/config', (req, res) => {
res.json({ env: process.env.NODE_ENV });
});
}
// Error handler - never expose stack in production
app.use((err, req, res, next) => {
logger.error('Request error', { error: err.message, stack: err.stack });
if (process.env.NODE_ENV === 'production') {
res.status(500).json({ error: 'Internal server error' });
} else {
res.status(500).json({
error: err.message,
stack: err.stack
});
}
});
module.exports = app;
Why this works:
- Winston logger with environment-based log levels
- Console output only in development
- Logs username only, never passwords
- Debug routes disabled in production (NODE_ENV check)
- Error handler shows stack traces only in development
Secure Pattern: Proper Error Handling (Java/Spring Boot)
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
@RestController
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
@Autowired
private Environment env;
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
// Correct: log username only
logger.info("Login attempt for: {}", request.getUsername());
// NEVER log password
User user = authService.authenticate(
request.getUsername(),
request.getPassword()
);
if (user == null) {
logger.warn("Failed login: {}", request.getUsername());
return ResponseEntity.status(401)
.body(new LoginResponse("Invalid credentials"));
}
logger.info("Successful login: {}", request.getUsername());
return ResponseEntity.ok(new LoginResponse(tokenService.create(user)));
}
// Debug endpoint only in dev
@GetMapping("/debug/config")
public ResponseEntity<?> debugConfig() {
if (!Arrays.asList(env.getActiveProfiles()).contains("dev")) {
return ResponseEntity.status(404).build();
}
return ResponseEntity.ok(Map.of("env", env.getActiveProfiles()));
}
}
// application.properties
// production:
// server.error.include-stacktrace=never
// server.error.include-message=never
// logging.level.root=INFO
Why this works:
- SLF4J logging framework with proper log levels
- Debug endpoints return 404 in non-dev profiles
- Stack traces never included in production responses (server.error.include-stacktrace=never)
- Logs username only, never passwords
- Profile-based configuration separates dev from production
Dynamic Scan Guidance
For guidance on remediating this CWE when detected by dynamic (DAST) scanners:
- Dynamic Scan Guidance - Analyzing DAST findings and mapping to source code
Security Checklist
- DEBUG=False in production (Flask, Django)
- NODE_ENV=production (Node.js)
- displayErrors=Off (PHP)
- No console.log(), print(), System.out() with sensitive data
- Debug endpoints (/debug/*) disabled or removed
- Stack traces not shown to users
- Log level is INFO or WARNING in production (not DEBUG)
- Logs filtered for passwords, tokens, credit cards