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:
- Enumerate the filesystem structure by calling
GET /credentialsand parsing the returned paths - Locate credential files by discovering the exact path to
credentials.json - Plan targeted attacks knowing the precise location of sensitive files
- Exploit path-based vulnerabilities like symlink attacks or race conditions with exact path knowledge
- 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
- Eliminates reconnaissance data: Attackers no longer learn the filesystem structure or exact paths
- Preserves functionality: The application still knows whether directories are configured and files exist
- Maintains diagnostics: Developers can still troubleshoot by knowing if components are configured, just not the exact paths
- 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
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
- CWE-209: Information Exposure Through an Error Message
- OWASP A01:2021: Broken Access Control
- OWASP A04:2021: Insecure Deserialization
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
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
- CWE-209: Information Exposure Through an Error Message
- OWASP: Sensitive Data Exposure
- OWASP API Security: API3:2019 Excessive Data Exposure
- Semgrep Rule: Information Disclosure in Error Messages
- fix: fix security issue in plugin_api.py