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
•Technically reviewed by Anupam Mediratta•Published September 5, 2026•Reviewed 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_dir → monitor_dir_configured (boolean)
- Line 85: env_monitor_dir → env_monitor_dir_set (boolean)
- Line 87: config_file → config_file_exists (boolean)
- Line 108: Removed path from detail message
- Line 123: Removed path from credentials detail
- Line 138: monitor_dir → monitor_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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #143

Related Articles

critical

Discord Bot Auto-Role CWE-269: Privilege Escalation via role.position

A Discord.js bot's auto-role feature allowed privilege escalation where any user with ManageRoles permission could add Administrator roles to automatic assignment lists, regardless of their own role hierarchy. The vulnerability existed because role.position validation checked only the bot's permissions, not the invoking user's role hierarchy relative to the target role.

high

CVE-2026-54290: hono cors() Reflects Any Origin With Credentials

The `hono` CORS middleware, as resolved in this service at 4.12.8, reflected the caller's `Origin` header back in `Access-Control-Allow-Origin` while also emitting `Access-Control-Allow-Credentials: true` whenever the `origin` option was left at its `'*'` default. That combination makes any website a trusted origin for credentialed cross-origin reads. The dependency range was raised from `^4.7.1` to `^4.13.5`, moving the installed copy from 4.12.8 to 4.13.5.

critical

POST /api/generate Lacks Authentication, Allowing Unauthenticated

A resume generation endpoint in a Node.js backend accepted requests from any caller with network access, allowing attackers to consume OpenAI API quota without restriction. The vulnerability stemmed from missing authentication middleware on a cost-bearing endpoint. The fix adds mandatory API key validation via HTTP headers before processing any generation requests.

critical

SchedulePush.disableReminder Missing Authorization Check in push.js

The `disableReminder` method in the `SchedulePush` class allowed any user to disable push notification reminders for arbitrary user IDs by manipulating the `e.user_id` event parameter. The fix adds a `checkFriend()` authorization gate that verifies the requesting user has a valid friendship relationship with the bot before modifying subscription state.

critical

How Broken Object-Level Authorization happens in Express.js and how to fix it

A critical authorization flaw in `src/v1/routes/index.js` allowed any authenticated API key holder to access arbitrary budgets by manipulating the `budgetSyncId` URL parameter. The fix introduces an environment-based allowlist that validates budget access before processing requests.

high

How Missing Rate Limiting Enables Denial of Service Attacks in Node.js and How to Fix It

The k-skill-proxy server exposed multiple public API endpoints (`/health`, `/v1/vworld/search`, `/v1/fine-dust/report`, `/v1/assembly/bills`) without consistent rate limiting middleware, leaving them vulnerable to denial-of-service attacks. A `buildRateLimiter` function existed but wasn't applied to all endpoints. This fix ensures rate limiting is enforced on all public endpoints, preventing resource exhaustion attacks.