Skip to content
Information Disclosure

Information Disclosure, Explained: How Stack Traces, Stray Files and Banners Map Your Server

A stack trace, a forgotten .git directory or a version banner rarely breaks anything alone. Here is how verbose errors, deployment leftovers and leaked internals hand attackers a map, how those leaks chain into real compromise, and how to keep diagnostics on the server.

Part 1 of 3From the Information Disclosure series

At a glance

CWE-200
Interpreter
Error handlers, web servers and whatever is deployed under the document root
Required condition
Diagnostics, internal identifiers or deployment files reach responses that any client can request.
Potential impact
Reconnaissance that speeds up targeted attacks, and direct compromise when source code, credentials or keys leak.
Primary defense
Generic error responses with server-side logging, build-only deployments and minimal version banners.

Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.

On this page

Information disclosure is any response that tells a stranger something the application never meant to publish: a stack trace with file paths and SQL, a Server header with an exact version, a .git directory in the web root, a configuration backup, a private IP address in an error. OWASP Top 10:2025 maps CWE-200, Exposure of Sensitive Information to an Unauthorized Actor, to A01 Broken Access Control, and CWE-209, sensitive error messages, to A10 Mishandling of Exceptional Conditions.

Most leaks are reconnaissance: they turn guessing into a lookup and tell an attacker which bug to try first. Some skip that step, because leaked source code, credentials and signing keys are the breach.

The mental model: diagnostics meant for you, read by everyone

Leak Typical source What it tells an attacker
Stack trace Unhandled exception rendered to the client Framework, file layout, query text, library versions
Version banner Server, X-Powered-By, generator meta tag Which published vulnerabilities to try
/.git/ directory Repository checked out into the web root Source code and its full history
.env, config.php.bak, dump.sql Deployment and editing leftovers Credentials, keys, database contents
Private IP or internal hostname Error text, comments, redirects Network layout and SSRF targets
Hard-coded key Inline configuration in a page Direct access to a third-party API

The rule behind the table: anything an unauthenticated client can request is published. A response is not private because nothing links to it.

How it happens

Three habits cause almost every case.

Debug settings reach production. ASPNETCORE_ENVIRONMENT=Development, Django's DEBUG = True, PHP's display_errors = On and Express without NODE_ENV=production all send exception details to the browser. Hand-written handlers do it on purpose, because a trace is convenient during development:

import traceback

@app.errorhandler(Exception)
def handle_error(exc):
    return {"error": str(exc), "trace": traceback.format_exc()}, 500

The deployment is a copy of the working directory. A git pull into the document root brings .git/ along. Editors leave config.php~ and .swp files, someone saves config.php.bak before an edit, a database dump lands next to index.php, and the web server serves it all.

Defaults announce themselves. Servers and runtimes put names and versions in Server and X-Powered-By, CMS platforms add a generator meta tag, and session cookies such as JSESSIONID, PHPSESSID and laravel_session name the stack.

A concrete walk-through: one error, one file, one map

An authorized tester starts with a value the application does not expect, such as a non-numeric order ID:

GET /orders/abc HTTP/1.1
Host: shop.example

With the handler above, the reply reads like documentation:

HTTP/1.1 500 Internal Server Error
Server: nginx/1.24.0
Content-Type: application/json

{"error": "invalid input syntax for type integer: \"abc\"",
 "trace": "Traceback (most recent call last):\n  File \"/srv/shop/releases/2026-09-30/app/orders.py\", line 41, in show_order\n    cur.execute(\"SELECT id, total FROM orders WHERE id = %s\", (order_id,))\n..."}

One response reveals the proxy and its version, Python with PostgreSQL, the deployment path, a table and its columns, and that this query is parameterized. The dated release directory hints at a checkout-based deployment, so the tester asks for one more file:

GET /.git/HEAD HTTP/1.1
Host: shop.example
HTTP/1.1 200 OK
Content-Type: application/octet-stream

ref: refs/heads/main

That line proves the repository is served. An authorized assessment stops and reports it: the objects behind it rebuild the source, including deleted files and every committed secret. The same one-request proof works for /.env, where a variable name is evidence enough, and for a .bak copy of a script, served as text instead of executed.

What an attacker gains

  • A banner or trace becomes a CVE lookup. An exact version maps straight to published advisories; see vulnerable components.
  • An absolute path aims file attacks. Knowing the code lives in /srv/shop/releases/ turns a blind path traversal guess into a precise read.
  • Query text shapes injection. Errors that echo SQL feed error-based SQL injection.
  • Private addresses become targets. A 10.0.4.17 in an error names a host for SSRF.
  • Source and configuration hand over keys. A .env file or repository history yields passwords, API keys and signing secrets, enough to forge tokens or sign in directly.
  • Email addresses seed phishing and supply usernames for password spraying.

A realistic chain needs no exploit: the banner names the framework, .git yields the source, history yields a signing key, and the key mints an administrator session.

Reading a SelfSec finding

The checks are passive and run on every crawled URL, so one leak in a shared template becomes many findings that one fix clears.

  • Each check reports its first match per URL. A public support address in the footer is a low finding on every page; keeping it is a decision, not a bug.
  • The stack-trace check matches the words stack trace and traceback, Java frames such as at com.example.Shop.load(Shop.java:42) and Python File "…", line N frames. Test other runtimes' error paths by hand.
  • The secret check looks for assignments such as apiKey: "…" or secret_key = '…' with a quoted value of 16 or more letters and digits. A quoted JSON key such as "apiKey": "…" does not match. Keys in bundles and source maps belong to the Client-Side Secret Scanner; see exposed secrets in JavaScript.
  • The mixed-content check flags src, href and action attributes pointing at http://, plain links included; see TLS misconfiguration.
  • Technology Fingerprint findings are informational; the evidence is the technology name or generator string. A Server header with a version, such as nginx/1.24.0, and any X-Powered-By header are also low findings from the security headers check.
  • The crawl requests leftovers such as /.git/config, /.env and .bak copies of discovered files, and those that answer join the scanned surface. Review them even when no pattern fires.
  • Development servers and framework debug pages have their own module; see debug mode exposure.

The fix: keep diagnostics on the server

Fix the error path first: log everything, return a status, a generic message and an incident ID. In Flask:

import logging
import uuid

from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException

app = Flask(__name__)
log = logging.getLogger(__name__)

@app.errorhandler(Exception)
def handle_error(exc):
    if isinstance(exc, HTTPException):
        return exc
    incident = uuid.uuid4().hex
    log.error("Unhandled error, incident %s", incident, exc_info=exc)
    return jsonify(error="Internal server error", incident=incident), 500

HTTPException passes through so 404 and 405 behave normally; everything else is logged with its traceback and answered with a fixed body. Leave FLASK_DEBUG unset in production.

In ASP.NET Core:

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options => options.AddServerHeader = false);
builder.Services.AddProblemDetails();

var app = builder.Build();
app.UseExceptionHandler();

The handler logs the exception and returns a problem details body with the status, a generic title and a traceId; AddServerHeader = false drops Server: Kestrel. Never run a reachable host as Development, which turns on the developer exception page. In PHP, php.ini logs instead of displaying, and expose_php = Off removes X-Powered-By:

display_errors = Off
log_errors = On
expose_php = Off

Next, publish only what you meant to. Deploy a build artifact, such as CI output, a container image or git archive, not a checkout; keep secrets in environment variables or a secret store; serve the public directory alone. Then add an nginx backstop ahead of other regex locations:

server_tokens off;

location ~ /\.(?!well-known/) {
    return 404;
}

location ~* (?:\.(?:bak|old|orig|save|swp|sql|dump)|~)$ {
    return 404;
}

A 404, unlike a 403, does not confirm the file exists. If anything sensitive was ever reachable, rotate it: deletion does not recall downloaded copies.

Fixes that do not hold

  • A friendly error page for HTML only: the JSON API or a proxy error page can still print the trace.
  • Blocking /.git/ on one host: another virtual host or the CDN origin may serve it. Remove it from disk.
  • Deleting a committed secret: it stays in history and every clone; rotate it.
  • Listing private paths in robots.txt: Disallow: /backup/ advertises the directory.
  • Hiding the banner instead of patching: behavior still fingerprints the stack.
  • Moving the trace into an HTML comment: it is still in the response.

A developer review checklist

  1. Trigger errors with malformed IDs, wrong types and unsupported methods, and read every response.
  2. Confirm production flags: ASPNETCORE_ENVIRONMENT, DEBUG, FLASK_DEBUG, NODE_ENV=production and display_errors.
  3. Deploy build output only; the document root holds no .git, .env, *.bak, editor files or dumps.
  4. Keep secrets out of the web root and client code; rotate anything that leaked.
  5. Remove version banners: server_tokens off, expose_php = Off, disabled X-Powered-By, no Server: Kestrel.
  6. Search responses for private IPs, internal hostnames, absolute paths and HTML comments.
  7. Return an incident ID to the client and keep the details in access-controlled logs.

References

Module: Information Disclosure and Technology Fingerprint. How it probes: Passive: no payload is sent. After the crawl each discovered URL is fetched again with a plain GET. Information Disclosure searches the body for email addresses, stack-trace text (the words stack trace or traceback, Java stack frames and Python file-and-line frames), apiKey, secret_key or access_token assignments with a quoted value of 16 or more letters and digits, internal directory paths, RFC 1918 private addresses, and src, href or action attributes pointing at http://. Technology Fingerprint reads Server, X-Powered-By and X-Generator values, X-Django headers, framework session cookies such as JSESSIONID, PHPSESSID, ASP.NET_SessionId and laravel_session, WordPress and Drupal asset paths and the generator meta tag. The crawl's wordlist discovery also requests source-control, environment, configuration and backup paths such as /.git/config, /.env and .bak copies of discovered files, and those that answer 200, 401 or 403 without being a catch-all page join the surface these checks read. How it confirms: Nothing is replayed: the response is the evidence. Each check reports its first match on a URL as its own finding, with the matched text and the request and response attached. A quoted API key or secret is high severity; stack-trace text (CWE-209) and http:// resource references (CWE-311) are medium; email addresses, internal paths and private IP addresses are low. Every detected technology is a separate informational finding whose evidence is the technology name or the full generator string. A finding moves from Possible to Confirmed only after this check. Reported as CWE-200, CWE-209, CWE-311 · ATT&CK T1213 · SARIF 2.1.0.
How SelfSec proves this one
Module
Information Disclosure and Technology Fingerprint
How it probes
Passive: no payload is sent. After the crawl each discovered URL is fetched again with a plain GET. Information Disclosure searches the body for email addresses, stack-trace text (the words stack trace or traceback, Java stack frames and Python file-and-line frames), apiKey, secret_key or access_token assignments with a quoted value of 16 or more letters and digits, internal directory paths, RFC 1918 private addresses, and src, href or action attributes pointing at http://. Technology Fingerprint reads Server, X-Powered-By and X-Generator values, X-Django headers, framework session cookies such as JSESSIONID, PHPSESSID, ASP.NET_SessionId and laravel_session, WordPress and Drupal asset paths and the generator meta tag. The crawl's wordlist discovery also requests source-control, environment, configuration and backup paths such as /.git/config, /.env and .bak copies of discovered files, and those that answer 200, 401 or 403 without being a catch-all page join the surface these checks read.
How it confirms
Nothing is replayed: the response is the evidence. Each check reports its first match on a URL as its own finding, with the matched text and the request and response attached. A quoted API key or secret is high severity; stack-trace text (CWE-209) and http:// resource references (CWE-311) are medium; email addresses, internal paths and private IP addresses are low. Every detected technology is a separate informational finding whose evidence is the technology name or the full generator string.
Reported as CWE-200, CWE-209, CWE-311 · ATT&CK T1213 · SARIF 2.1.0

A reflected response alone is not enough to promote a Information Disclosure finding. The classical engine has to confirm the behavior before it reaches the report. See the detection engine

SelfSec scanner

Find Information Disclosure in your own app

Reading about the class is one thing. SelfSec tests for it against an application you are authorized to assess, and only calls it real when independent evidence agrees.

Find it Confirmed, not guessed 21 vulnerability modules, with the classical engine owning the verdict. Prove it Evidence you can re-read Every finding keeps the raw request, the response and the baseline it was measured against.
Join the launch list