Back to Blog
critical SEVERITY6 min read

How Information Disclosure Vulnerabilities Happen in Python APIs and How to Fix It

A critical information disclosure vulnerability in the Hermes plugin dashboard API was exposing sensitive filesystem paths, credential file locations, and configuration details without authentication. The fix redacts this sensitive information from API responses, replacing absolute paths with boolean status indicators to prevent attackers from locating and targeting credential files.

O
By Orbis AppSec
Published September 5, 2026Reviewed September 5, 2026

Answer Summary

This is an information disclosure vulnerability (CWE-200) in a Python Flask API that exposed sensitive filesystem paths, monitor directories, and credential file locations through unauthenticated endpoints. The fix replaces path disclosures with boolean status indicators in the get_config() and get_doctor() endpoints, preventing attackers from gaining reconnaissance information needed to locate and steal credentials.

Vulnerability at a Glance

cweCWE-200 (Exposure of Sensitive Information to an Unauthorized Actor)
fixReplace path disclosures with boolean existence indicators; remove directory path details from error messages
riskAttackers can enumerate filesystem structure, locate credential files, and plan targeted attacks
languagePython (Flask)
root causeAPI endpoints returning absolute filesystem paths and file existence status without authentication or data masking
vulnerabilityInformation Disclosure / Path Traversal Reconnaissance

How Information Disclosure Vulnerabilities Happen in Python APIs and How to Fix It

In the Hermes plugin dashboard, we discovered a critical information disclosure vulnerability in hermes-plugin/dashboard/plugin_api.py that exposed sensitive filesystem paths, credential file locations, and system configuration details through unauthenticated API endpoints. This vulnerability could have allowed attackers to perform reconnaissance, locate credential files, and plan targeted attacks against the application.

The Vulnerability Explained

What Was Exposed?

The vulnerable code in plugin_api.py contained two endpoints—get_config() and get_doctor()—that returned sensitive information without proper redaction:

Vulnerable Code (Before Fix):

def get_config() -> Dict[str, Any]:
    try:
        return {
            "ok": True,
            "monitor_dir": pm._resolve_monitor_dir(),           # ❌ Full path exposed
            "node_exe": pm._resolve_node_exe(),
            "env_monitor_dir": os.environ.get("VRC_MONITOR_DIR"),  # ❌ Environment path exposed
            "env_node_exe": os.environ.get("VRC_MONITOR_NODE"),
            "config_file": str(pm._config_path()),              # ❌ Full filesystem path exposed
        }

And in the get_doctor() endpoint:

checks.append({
    "name": "凭据文件",
    "ok": cred_ok,
    "detail": f"credentials.json {'存在' if cred_ok else '不存在'},位于 {monitor_dir}",  # ❌ Path in error detail
})

# Later in response:
"resolved": {
    "monitor_dir": monitor_dir,  # ❌ Absolute path in diagnostic output
    "node_exe": node_exe,
}

Why This Is Dangerous

These endpoints returned absolute filesystem paths like /home/user/vrc-agents/monitor and /home/user/.config/vrc/credentials.json without any authentication requirements. An attacker could:

  1. Enumerate the filesystem structure by calling GET /credentials and parsing the returned paths
  2. Locate credential files by discovering the exact path to credentials.json
  3. Plan targeted attacks knowing the precise location of sensitive files
  4. Exploit path-based vulnerabilities like symlink attacks or race conditions with exact path knowledge
  5. Perform reconnaissance for privilege escalation or lateral movement

The vulnerability was particularly severe because:
- No authentication was required to call these endpoints
- The information enabled a 2-step attack chain: first, reconnaissance through path disclosure; second, targeted file theft or manipulation
- The endpoints were diagnostic/configuration endpoints that developers might assume were safe to expose

Real-World Attack Scenario

An attacker could:

# Step 1: Reconnaissance - Call unauthenticated endpoint
curl http://target-app:3000/api/config
# Response reveals: /home/user/vrc-agents/monitor and /home/user/.config/vrc/credentials.json

# Step 2: Exploit - Now knowing exact path, attempt to:
# - Read credentials via symlink attack
# - Exploit file permissions using the known path
# - Target the specific directory for brute-force attacks
# - Map the entire application filesystem structure

The Fix

The fix redacts all sensitive filesystem paths and replaces them with boolean status indicators that preserve functionality while eliminating reconnaissance information:

Fixed Code (After Fix):

def get_config() -> Dict[str, Any]:
    try:
        return {
            "ok": True,
            "monitor_dir_configured": pm._resolve_monitor_dir() is not None,  # ✅ Boolean instead of path
            "node_exe": pm._resolve_node_exe(),
            "env_monitor_dir_set": bool(os.environ.get("VRC_MONITOR_DIR")),   # ✅ Boolean instead of path
            "env_node_exe": os.environ.get("VRC_MONITOR_NODE"),
            "config_file_exists": pm._config_path().is_file(),                 # ✅ Boolean instead of path
        }

And in get_doctor():

checks.append({
    "name": "服务目录",
    "ok": monitor_dir is not None,
    "detail": "已解析" if monitor_dir else "未找到服务目录:...",  # ✅ Generic status, no path
})

checks.append({
    "name": "凭据文件",
    "ok": cred_ok,
    "detail": f"credentials.json {'存在' if cred_ok else '不存在'}",  # ✅ No path information
})

# Later in response:
"resolved": {
    "monitor_dir_configured": monitor_dir is not None,  # ✅ Boolean instead of path
    "node_exe": node_exe,
}

Why This Fix Works

  1. Eliminates reconnaissance data: Attackers no longer learn the filesystem structure or exact paths
  2. Preserves functionality: The application still knows whether directories are configured and files exist
  3. Maintains diagnostics: Developers can still troubleshoot by knowing if components are configured, just not the exact paths
  4. Follows security principles: Implements "least privilege" for information disclosure

The changes were made at:
- Line 83: monitor_dirmonitor_dir_configured (boolean)
- Line 85: env_monitor_direnv_monitor_dir_set (boolean)
- Line 87: config_fileconfig_file_exists (boolean)
- Line 108: Removed path from detail message
- Line 123: Removed path from credentials detail
- Line 138: monitor_dirmonitor_dir_configured in resolved output

Prevention & Best Practices

1. Apply Information Minimization Principle

Never expose more data than necessary. Ask: "Does the client need this exact path, or just need to know if it's configured?"

# ❌ BAD: Exposes full path
def get_status():
    return {"config_path": os.path.expanduser("~/.app/config.json")}

# ✅ GOOD: Only exposes status
def get_status():
    config_path = os.path.expanduser("~/.app/config.json")
    return {"config_exists": os.path.exists(config_path)}

2. Redact Sensitive Data in Error Messages

Generic error messages prevent information disclosure:

# ❌ BAD: Reveals system paths
try:
    with open(cred_file) as f:
        credentials = json.load(f)
except FileNotFoundError as e:
    return {"error": f"Credentials file not found at {cred_file}"}

# ✅ GOOD: Generic error message
try:
    with open(cred_file) as f:
        credentials = json.load(f)
except FileNotFoundError:
    return {"error": "Configuration error. Please contact administrator."}

3. Use Static Analysis to Catch Path Disclosures

Semgrep rules can flag patterns like returning os.path results or environment variables in API responses:

rules:
  - id: path-disclosure-in-api-response
    pattern: |
      return {
        ...,
        $KEY: $PATH,
        ...
      }
    where:
      - $PATH in (os.path.expanduser(...), os.path.abspath(...), os.environ.get(...))
    message: "Avoid exposing filesystem paths in API responses"

4. Implement Authentication and Authorization

Even diagnostic endpoints should require authentication:

from functools import wraps

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not request.headers.get('Authorization'):
            return {"error": "Unauthorized"}, 401
        return f(*args, **kwargs)
    return decorated

@app.route('/api/config')
@require_auth
def get_config():
    # ... endpoint code

5. Reference Security Standards

Key Takeaways

  • Never return absolute filesystem paths in API responses, even in diagnostic endpoints. Use boolean status indicators instead.
  • The get_config() endpoint in plugin_api.py line 83-87 exposed three separate path disclosures that collectively enabled reconnaissance attacks.
  • Information disclosure is often overlooked because it doesn't cause immediate crashes or data corruption, but it's a critical first step in multi-stage attacks.
  • Generic error messages are security features, not poor UX. The fix replaced specific path details with status messages like "已解析" (resolved) that help debugging without aiding attackers.
  • Redaction should preserve functionality: The application still knows if directories are configured; it just doesn't broadcast the exact paths to unauthenticated callers.

How Orbis AppSec Detected This

Source: Unauthenticated HTTP GET request to /api/config and /api/doctor endpoints

Sink: Return statements in get_config() (line 83-87) and get_doctor() (lines 108, 123, 138) that serialize filesystem paths directly into JSON responses

Missing Control: No data sanitization or redaction layer; no authentication requirement; no validation that returned data doesn't contain sensitive information

CWE: CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor)

Fix: Replace all absolute filesystem paths with boolean existence indicators (monitor_dir_configured, env_monitor_dir_set, config_file_exists) and remove path details from diagnostic messages

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

Information disclosure vulnerabilities are often underestimated because they don't directly cause data loss or system compromise—they enable reconnaissance. The Hermes plugin dashboard vulnerability exemplifies this: by exposing filesystem paths, it handed attackers a map of the system and the exact location of credential files.

The fix demonstrates a critical security principle: minimize information exposure. Even internal or diagnostic endpoints should follow the principle of least privilege when it comes to data disclosure. Use boolean indicators instead of paths, generic error messages instead of specific details, and authentication/authorization checks on all sensitive endpoints.

As developers, we should ask ourselves: "Does the client truly need this information, or just need to know the status?" In most cases, the answer is the latter—and that distinction can mean the difference between a secure application and one vulnerable to reconnaissance attacks.


References

Frequently Asked Questions

What is information disclosure in API design?

Information disclosure occurs when an API exposes sensitive data like filesystem paths, configuration details, or system information to unauthorized users, enabling reconnaissance for further attacks.

How do you prevent information disclosure in Python APIs?

Sanitize all API responses to remove sensitive details (paths, versions, internal IDs), implement authentication/authorization checks, use generic error messages, and follow the principle of least privilege for data exposure.

What CWE is information disclosure?

CWE-200 covers exposure of sensitive information to unauthorized actors, with related issues in CWE-209 (Information Exposure Through an Error Message) and CWE-538 (Use of Persistent Cookies Containing Sensitive Information).

Is authentication alone enough to prevent information disclosure?

No. Even authenticated users should only receive the minimum necessary data. This vulnerability existed in an unauthenticated endpoint, making it especially dangerous, but authenticated endpoints must also avoid leaking sensitive paths and system details.

Can static analysis detect information disclosure vulnerabilities?

Yes. Tools can flag patterns like returning os.path results, exposing environment variables, or including filesystem paths in response objects, though semantic analysis is needed to distinguish between safe and unsafe disclosures.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #143

Related Articles

critical

How Missing Authentication on DELETE Endpoints Happens in Node.js Express and How to Fix It

A critical authentication bypass vulnerability was discovered in the skill-cabinet server where the DELETE /api/skills/:id endpoint allowed any unauthenticated user to delete arbitrary skills from the filesystem. The fix implements loopback origin validation to ensure only requests from localhost can perform destructive operations, while also consolidating delete functionality into a single, protected endpoint.

critical

How Rate Limiting Vulnerabilities Happen in Next.js API Routes and How to Fix It

A critical rate limiting vulnerability in the `/api/claim` endpoint allowed attackers to exhaust the shared GitHub API quota by sending unlimited rapid requests. While the `/api/records` endpoint had proper throttling, the claim route only checked for GitHub rate limiting responses but implemented no per-user rate limiting, enabling abuse of the shared `REGISTRY_TOKEN` quota.

high

How Cross-Site Request Forgery (CSRF) happens in Express.js and how to fix it

A semgrep audit flagged `devboard/server/index.js` for lacking any CSRF middleware, meaning every state-changing route (`POST`, `PUT`, `DELETE` under `/api/*`) could be triggered by a forged cross-origin request riding on a victim's session cookie. The fix wires in `cookie-parser` and `csurf` right after body parsing, so every mutating request now requires a valid, per-session CSRF token before it reaches route handlers.

critical

How missing authorization and code injection happen in Mindustry JavaScript mods and how to fix it

A `TapEvent` handler in `scripts/CommandBlock.js` exposed a full administrative command palette — including a `run-javascript` command that piped player-supplied text straight into `new Function(text)()` — behind nothing more than a team-membership check. Any player who happened to share a team with the block could execute arbitrary JavaScript (and, through Rhino's Java bridge, arbitrary host code) inside the game runtime. The fix adds an explicit `if (!e.player.admin) return;` guard at the top

critical

How Unauthenticated API Exposure Happens in Node.js Koa Routers and How to Fix It

The `/api/adapters` and `/api/list` endpoints in the OneBots framework were registered before authentication middleware, making them publicly accessible to unauthenticated attackers. This critical vulnerability allowed anyone to enumerate all configured adapters, accounts, and sensitive metadata with a simple GET request. The fix ensures these endpoints are protected by the existing auth middleware by correcting route registration order.

critical

How Hardcoded Firebase API Keys Happen in JavaScript Service Workers and How to Fix Them

A production Firebase service worker file (`static/firebase-messaging-sw.js`) contained hardcoded API keys and project configuration directly in publicly accessible JavaScript — including a second, previously commented-out set of credentials from an older project. While Firebase web config values are technically public identifiers, pairing them with unrestricted Firebase projects or missing Security Rules turns them into an open door for push notification abuse, quota exhaustion, and unauthorize