Skip to content

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. The disclosure is the harm in itself, and it also gives an attacker reconnaissance for further attacks.

Relationship to Other CWEs

CWE-215 is a child of CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor). The CWE-200 router page lists the siblings, in case another one describes the problem better.

  • CWE-215 (this page) - sensitive values exposed by debugging instrumentation: debug statements, verbose logging, stack traces, and development-only features left reachable in production.
  • CWE-489 (Active Debug Code) - covers any debug instrumentation left active in production - test backdoors, disabled authentication, debug UI - even when it does not leak sensitive data. CWE-215 is the specific case where that debug code inserts or exposes sensitive information such as credentials, tokens, or internal configuration. Use CWE-215 when the problem is what leaks; use CWE-489 when the problem is that debug functionality is reachable at all, regardless of content.
  • CWE-209 (Generation of Error Message Containing Sensitive Information) - applies to sensitive detail leaking through normal error handling, independent of any debug flag. CWE-215 applies when the leak comes from debug instrumentation - a debug flag, a debug-only handler, a logger.debug() call - rather than the application's ordinary error path.
  • CWE-532 (Insertion of Sensitive Information into Log File) - covers sensitive data written to log files from any source. CWE-215 is narrower: it is about debug-level instrumentation - verbose or debug log statements, debug endpoints, debug-mode error output - rather than logging in general.

OWASP Classification

A10:2025 - Mishandling of Exceptional Conditions

Risk

Medium: Debug code in production leaks whatever it touches: credentials and tokens captured by debug log statements, secrets and the database URL dumped by a debug configuration route, SQL text and file paths returned in a stack trace. Credentials exposed this way are usable directly; the rest maps the application's internals for a follow-up attack.

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

Debug code that leaks usually takes one of these forms:

  • Configuration settings that enable verbose error output
  • Log statements that record sensitive data such as passwords, tokens, or API keys
  • Routes that expose configuration, environment variables, or system information
  • Exception handling that reveals stack traces or internal details to users
  • Commented or conditional debug code that might execute in production

Disable Debug Mode in Production

Control debug mode from environment configuration and default it to off:

  • Set debug flags to False in production configuration
  • Use environment-based configuration classes (Development vs Production)
  • Never hardcode DEBUG=True or equivalent settings
  • Disable verbose error output (stack traces, detailed messages)
  • Use production-grade web servers, not development servers

Remove Debug Endpoints and Logging

Remove debug code that could leak information:

  • Remove debug routes (/debug/*, /test/*, /admin/debug) from production builds - register them only when the environment is not production, so there is no handler left for a runtime flag to re-enable
  • Remove debug log statements that record passwords, tokens, credit cards, or API keys; log levels and redaction are covered under Configure Secure Logging below

Implement Generic Error Handling

Prevent stack traces and internal details from reaching users:

  • Configure the framework to never include stack traces in responses
  • Add 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

Set up logging so it cannot capture sensitive values:

  • Use structured logging frameworks (Winston, SLF4J, Python logging) instead of console output
  • Set log levels by environment (INFO/WARNING in production, DEBUG in development)
  • Add log filters that 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 disabled in production:

  • Verify debug mode is off in production configuration. The setting differs per stack, and in every case check the value the running process actually reports, not the one in the committed config file:
    • Flask: FLASK_DEBUG=0 and flask run without --debug. Setting DEBUG in code may not take effect, because Flask derives its debug state from FLASK_DEBUG.
    • Django: DEBUG = False in the settings module that DJANGO_SETTINGS_MODULE actually loads. Django reads no DEBUG environment variable of its own, so an env var by that name changes nothing unless the settings module reads it.
    • Node.js: NODE_ENV=production, which also switches many frameworks to terse error output.
    • PHP: display_errors=Off together with display_startup_errors=Off in php.ini.
  • Confirm debug endpoints return 404 or are completely absent
  • Test that errors return generic messages without stack traces
  • Review logs and confirm no sensitive data is recorded
  • Production log level is INFO or WARNING - but treat this as containment, not proof. The logger.debug("... pass=" + password) call is still in the binary and its argument is still evaluated, so anything that raises the level at runtime (Spring Boot Actuator's /loggers endpoint, a Log4j2 auto-reconfigure, a Django LOGGING override) starts writing credentials again with no code change. The verification is that no sensitive value reaches a logging call at any level

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
config.debug = true                            // hardcoded, ships to production unchanged

function handle_login(request):
    log.debug("login attempt: user=" + username + " pass=" + password)   // credentials written to the log

route "/debug/config":
    return dump(app.config)                    // secrets, DB URL, API keys served to anyone who asks

function on_error(exception):
    return response(500, body=exception.full_trace)   // SQL text, file paths, internals returned to the client

Why this is vulnerable: A hardcoded or defaulted-on debug flag ships unchanged to production. Debug-level log statements capture raw request data - including passwords and tokens - into files that operations teams, backup systems, and log aggregation services can read. Debug routes and debug-mode error output hand an attacker the application's configuration, secrets, and internal structure as reconnaissance for further attacks.

Secure Patterns

// SECURE - pseudo-code
config.debug = read_env("DEBUG", default=false) and environment != "production"

function handle_login(request):
    log.info("login attempt: user=" + username)   // never the password, token, or hash
    if not authenticate(username, password):
        log.warn("login failed: user=" + username)
        return response(401, body="Invalid credentials")   // generic message, no reason exposed

if environment != "production":                 // route is never registered in a production build
    route "/debug/config":
        return dump(pick(app.config, ["VERSION", "ENVIRONMENT", "COMMIT_SHA"]))   // allowlist

function on_error(exception):
    log.error("request failed", exception, full_context=true)   // full detail kept server-side
    if config.debug:
        return response(500, body=exception.full_trace)          // development only
    return response(500, body="Internal server error")           // generic message everywhere else

Why this works: The debug flag reads from environment configuration that defaults closed, so a forgotten toggle can't ship debug mode to production. Logging captures the username for audit purposes but never the password or token. The debug route is registered only outside production, so a production build has no handler to reach at all - there is no flag left that a runtime config change, an environment variable, or a mistaken deploy can flip back on. What it returns is an explicit allowlist of harmless fields, so a STRIPE_SECRET or JWT_PRIVATE_KEY added to the config later stays out by default instead of leaking until someone remembers to add it to a blocklist. Error handling always logs full detail server-side, but only returns it to the client when debug mode is explicitly on - production always gets a generic message.

Common Pitfalls

  • Gating the debug flag with a commented-out line, not environment config: # DEBUG = True still ships if a later edit or merge re-enables it. The flag must read from environment/config that defaults closed, not from a line a developer has to remember to keep commented.
  • Filtering only the password field before logging: a debug logger that dumps the whole request or response object still leaks the Authorization header, a session cookie, or a nested token field the filter didn't anticipate. Allowlist which fields get logged; don't try to blocklist which fields don't.
  • Restricting debug endpoints by network location instead of removing them: an "internal-only" debug route is still reachable from anything that pivots inside the network - a compromised VPN client, an SSRF, a container escape. Gate on environment/build state, not network position, and strip the route from production builds entirely.
  • Truncating the exception instead of replacing it: except Exception: return str(e)[:100] still surfaces the start of a SQL query or a file path. The fix is a generic message, not a shorter version of the same leak.

Additional Resources