Summary
Two independent path traversal vulnerabilities allow a malicious skill to read arbitrary host files or write outside quarantine during skill installation.
Finding 1: credential_files.py — arbitrary host file mounting
File: tools/credential_files.py, register_credential_file() function
The relative_path argument (from skill frontmatter required_credential_files) is joined to HERMES_HOME via hermes_home / relative_path without canonicalization or containment check. A path like ../../.ssh/id_rsa resolves outside HERMES_HOME, and if the file exists, it is registered and mounted into Docker/Modal sandboxes.
Reproduction:
- Create a skill with
required_credential_files: ["../../../.ssh/id_rsa"] in its frontmatter
- Install and run the skill
- The SSH private key is mounted into the container at a predictable path
Suggested fix:
host_path = (hermes_home / relative_path).resolve()
if not str(host_path).startswith(str(hermes_home.resolve())):
logger.warning("credential_files: refusing out-of-bounds path %s", relative_path)
return False
Finding 2: skills_hub.py — quarantine escape via bundle file paths
File: tools/skills_hub.py, quarantine_bundle() function
The rel_path keys from bundle.files are used directly in dest / rel_path without validation. A malicious skill repository can include file entries like ../../.hermes/SOUL.md which write outside the quarantine directory — before the security scan runs.
Reproduction:
A malicious GitHub-hosted skill with a files dict containing {"../../.hermes/SOUL.md": "<injected identity>"} overwrites the agent's identity prompt when quarantine_bundle is called.
Suggested fix:
for rel_path, file_content in bundle.files.items():
candidate = (dest / rel_path).resolve()
if not str(candidate).startswith(str(dest.resolve())):
raise ValueError(f"Path traversal in bundle: {rel_path}")
Risk Assessment
| Aspect |
Detail |
| Severity |
High |
| Attack vector |
Malicious skill installation |
| Prerequisite |
User installs untrusted skill from community/URL source |
| Impact |
Read arbitrary host files (Finding 1), write arbitrary files (Finding 2) |
| Existing mitigation |
skills_guard scans skill content, but path traversal bypasses quarantine |
Both findings share the same root cause: unsanitized path joins with external input. The fix pattern (resolve + containment check) is standard.
Found during a community security review. We use Hermes Agent in production and wanted to contribute back.
Summary
Two independent path traversal vulnerabilities allow a malicious skill to read arbitrary host files or write outside quarantine during skill installation.
Finding 1: credential_files.py — arbitrary host file mounting
File:
tools/credential_files.py,register_credential_file()functionThe
relative_pathargument (from skill frontmatterrequired_credential_files) is joined toHERMES_HOMEviahermes_home / relative_pathwithout canonicalization or containment check. A path like../../.ssh/id_rsaresolves outsideHERMES_HOME, and if the file exists, it is registered and mounted into Docker/Modal sandboxes.Reproduction:
required_credential_files: ["../../../.ssh/id_rsa"]in its frontmatterSuggested fix:
Finding 2: skills_hub.py — quarantine escape via bundle file paths
File:
tools/skills_hub.py,quarantine_bundle()functionThe
rel_pathkeys frombundle.filesare used directly indest / rel_pathwithout validation. A malicious skill repository can include file entries like../../.hermes/SOUL.mdwhich write outside the quarantine directory — before the security scan runs.Reproduction:
A malicious GitHub-hosted skill with a
filesdict containing{"../../.hermes/SOUL.md": "<injected identity>"}overwrites the agent's identity prompt whenquarantine_bundleis called.Suggested fix:
Risk Assessment
Both findings share the same root cause: unsanitized path joins with external input. The fix pattern (resolve + containment check) is standard.
Found during a community security review. We use Hermes Agent in production and wanted to contribute back.