Skip to content

CWE-497: Exposure of Sensitive System Information to an Unauthorized Control Sphere

Overview

System information exposure is an application leaking internal detail - server and framework versions, absolute paths, database names, OS information - through error messages, response headers, API payloads, or page content. An attacker reads it as reconnaissance, and it narrows which exploits are worth trying.

Relationship to Other CWEs

CWE-497 is a child of CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor) - use the CWE-200 router page if this finding is better described by a different sibling. It is the parent of CWE-548 (Exposure of Information Through Directory Listing), the specific case where the exposure mechanism is a directory listing. MITRE also lists CWE-214 (Invocation of Process Using Visible Sensitive Information) as a further child, with no page here yet.

OWASP Classification

A01:2025 - Broken Access Control

Risk

Medium: Exposed system information names the software and versions running, along with internal paths and directory structure. That turns exploit selection from guesswork into a lookup against published CVEs.

Remediation Steps

Core Principle: Do not expose system internals - paths, versions, stack traces - beyond what a response needs to do its job.

Locate System Information Exposure

A scanner usually reports one leaking response. The same fingerprint is normally available from several places at once, so enumerate them rather than fixing the reported one:

  • Response headers: Server, X-Powered-By, X-AspNet-Version, X-Generator and any framework-specific equivalent - present on every response, including error and redirect responses
  • Error pages: stack traces, database driver messages, and absolute file paths, on both the framework's default error page and any custom one
  • Diagnostic endpoints: /info, /status, /version, /health, /actuator/* and similar, checked unauthenticated
  • 404 and 405 responses: a "file not found" that names the path it looked in, or an "allowed methods" list that describes the routing table
  • Body content: version strings in HTML comments, generator meta tags, and API payloads that report the build

Remove Version Headers and Technology Fingerprints (Primary Defense)

Strip identifying headers (Server, X-Powered-By, X-AspNet-Version) at both the application layer and the web server/reverse proxy layer - the proxy-level setting is what actually removes the Server header, since it's usually generated by the server software itself, not the application. Without a version string an attacker has to probe blindly rather than go straight to version-specific CVEs.

Use Generic Error Messages for Users, Log Details Server-Side

// VULNERABLE - exposes database internals to the caller
try:
    db.execute(query)
except SQLError as e:
    return 'Database error: ' + e.message, 500  // leaks e.g. "Table 'users' doesn't exist"

// SECURE - generic message to the user, full detail logged server-side only
try:
    db.execute(query)
except SQLError as e:
    log.error('database error executing query', e, context = { query_name: 'get_user_profile' })
    return { error: 'An error occurred processing your request' }, 500

Why this works: The diagnostic detail and the user-facing message become two separate things. The full exception goes to the server log, where access controls already apply; the caller gets a fixed string that says nothing about the database, the schema, or the query.

Disable Debug Pages and Verbose Output in Production

Debug/verbose modes (Django's DEBUG, Flask's debug=True, and equivalents elsewhere) must be False/off in production - they expose full stack traces, source snippets, local variable values, and settings. Pair this with an explicit allowlist of accepted hostnames (Django's ALLOWED_HOSTS or equivalent) rather than a wildcard, and register custom error handlers that render a generic page instead of the framework's default detailed error view.

Don't Expose Versions or Internals in API Responses

// VULNERABLE - a public endpoint hands out a full technology fingerprint
route('/api/info', () => json({ app: 'MyApp', version: '1.2.3', framework: 'Flask 2.0.1', database: 'PostgreSQL 12.3' }))

// SECURE - public status endpoint reveals nothing about the stack
route('/api/status', () => json({ status: 'operational' }))

// Detailed diagnostics, if needed at all, live behind authentication
route('/api/admin/info', require_admin, () => json({ version: '1.2.3', uptime: get_uptime() }))

Why this works: The public endpoint returns only what its callers need. Version and diagnostic detail sit behind an authorization check, so an unauthenticated scanner cannot collect a component-and-version list to cross-reference against CVE databases.

Test the Fix

  • Check response headers for version info with a real GET (curl -sS -D - -o /dev/null <url>) rather than curl -I, since a HEAD request can take a different path through the stack - server/framework version strings should be absent, not just the number stripped
  • Trigger errors deliberately (malformed input, invalid IDs) and verify only generic messages reach the client, never a stack trace, file path, or SQL error text
  • Request non-existent resources and confirm 404 pages don't disclose file system paths
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

Debug mode enabled in a deployed environment

// VULNERABLE - debug mode left on, exposes stack traces/paths/env vars to any client that triggers an error
app.config.debug = true

Why this is vulnerable: one setting changes what every error response contains, everywhere in the application at once. A framework's debug mode is built to be maximally informative to a developer sitting at the machine: stack traces naming every frame and file path, the framework and version, the settings object, often the local variables at the point of failure and sometimes an interactive console that evaluates expressions in the running process. None of that is a leak of the error - it is a description of the deployment, and it is served to whoever caused the error.

The reason this reaches production is that it produces no symptom until something fails. Every successful request behaves identically with the flag on or off, so the setting cannot be wrong in a way that testing notices, and the first person to see the difference is whoever triggers an exception - which an attacker can do deliberately with malformed input. Where the value is read from configuration, the failure mode to check for is a missing or unparsed variable defaulting to on.

The same setting is reported under CWE-489 as often as under CWE-497, and both are defensible: this page is the right one when the finding is the reconnaissance the flag discloses, CWE-489 when it is that a development-only path is live at all. The remediation is the same either way.

A raw exception message returned in the response

// VULNERABLE - a caught exception's raw message reaches the response
except FileNotFoundError as e:
    return e.message, 404   // e.g. "File not found: /var/www/app/data/secrets.txt"

Why this is vulnerable: the exception was caught, the status code is right, and the handling looks careful - which is why this survives review that would have caught an uncaught error. What passes through is the message, and the message was written to tell an operator what went wrong, so it carries the absolute path, the account name, or the host the code was reaching for.

The disclosure is also usable as an oracle rather than as a single fact. Because the message differs by cause, a caller can distinguish "this path does not exist" from "it exists and I may not read it", and walk the filesystem or the identifier space by watching which message comes back. Returning the same response for both, and putting the distinction in the log with a correlation identifier, closes that without losing anything the operator needs.

Secure Patterns

// SECURE - debug mode off, generic error handler in its place
app.config.debug = false

on_error(e):
    log.error('unhandled error', e, include_stack_trace = true)   // server-side only
    return { error: 'An error occurred' }, 500

Why this works: With debug mode off, the framework's detailed error page never renders. The handler that replaces it keeps the full diagnostic detail server-side and returns a fixed message, so an attacker who triggers an exception learns nothing about file paths, source structure, or the database.

Additional Resources