fix(credentials): validate mounted credential file paths - #3946
Closed
Gutslabs wants to merge 1 commit into
Closed
Conversation
Contributor
|
Thanks for the security-focused contribution! This fix has already been applied to
Evidence that the fix is already on main:
Closing as |
teknium1
pushed a commit
that referenced
this pull request
Jul 20, 2026
…dboxes
register_credential_file() takes a skill-declared relative path from
required_credential_files frontmatter and bind-mounts it read-only into the
remote sandbox the skill's own code runs in. It validates that the resolved
path stays inside HERMES_HOME — the docstring names the threat directly:
so that a malicious skill cannot declare
required_credential_files: ['../../.ssh/id_rsa'] and exfiltrate
sensitive host files into a container sandbox
Containment is the wrong boundary on its own, because HERMES_HOME is exactly
where the master credential stores live. Traversal is blocked; asking for the
keys by name is not:
skill declares mounted? agent may read it?
.env YES DENIED
auth.json YES DENIED
.anthropic_oauth.json YES DENIED
cache/bws_cache.json YES DENIED
mcp-tokens/srv.json YES DENIED
google_token.json YES allowed
../../.ssh/id_rsa no n/a
Every row marked DENIED is refused by the canonical read guard
(agent.file_safety.get_read_block_error) — the agent cannot read_file them —
yet one line of hub-installed skill frontmatter gets them bind-mounted where
that skill can cat them. .env alone is every provider API key.
Reuse the canonical deny-list as the mount bar: what the agent is forbidden
to read is not mountable either, so the mount surface cannot hand a skill
what the read surface denies it. Fails CLOSED — if the guard can't be
consulted the mount is refused rather than risked.
The module keeps doing its job: a skill still mounts its own service token
(google_token.json, skills/*), and a refused entry is reported back through
register_credential_files' missing list instead of failing the batch.
The three prior PRs here (#3946, #3951, #4316) all hardened traversal; this
closes the half that traversal validation never covered.
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…dboxes
register_credential_file() takes a skill-declared relative path from
required_credential_files frontmatter and bind-mounts it read-only into the
remote sandbox the skill's own code runs in. It validates that the resolved
path stays inside HERMES_HOME — the docstring names the threat directly:
so that a malicious skill cannot declare
required_credential_files: ['../../.ssh/id_rsa'] and exfiltrate
sensitive host files into a container sandbox
Containment is the wrong boundary on its own, because HERMES_HOME is exactly
where the master credential stores live. Traversal is blocked; asking for the
keys by name is not:
skill declares mounted? agent may read it?
.env YES DENIED
auth.json YES DENIED
.anthropic_oauth.json YES DENIED
cache/bws_cache.json YES DENIED
mcp-tokens/srv.json YES DENIED
google_token.json YES allowed
../../.ssh/id_rsa no n/a
Every row marked DENIED is refused by the canonical read guard
(agent.file_safety.get_read_block_error) — the agent cannot read_file them —
yet one line of hub-installed skill frontmatter gets them bind-mounted where
that skill can cat them. .env alone is every provider API key.
Reuse the canonical deny-list as the mount bar: what the agent is forbidden
to read is not mountable either, so the mount surface cannot hand a skill
what the read surface denies it. Fails CLOSED — if the guard can't be
consulted the mount is refused rather than risked.
The module keeps doing its job: a skill still mounts its own service token
(google_token.json, skills/*), and a refused entry is reported back through
register_credential_files' missing list instead of failing the batch.
The three prior PRs here (NousResearch#3946, NousResearch#3951, NousResearch#4316) all hardened traversal; this
closes the half that traversal validation never covered.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes a credential-file mount safety issue for remote execution backends.
Previously,
required_credential_filesentries from skill frontmatter were passed through as relative paths underHERMES_HOMEwithout validating traversal segments, absolute paths, or symlink escapes. That meant a skill could register entries like../outside.txtor an absolute path and have arbitrary host files added to the sandbox mount list.This change validates credential file paths as safe relative paths under
HERMES_HOME, rejects paths that resolve outside that root, and applies the same guard toterminal.credential_filesconfig entries.Type of Change
Changes Made
HERMES_HOMEHERMES_HOMEterminal.credential_filesHow to Test
source .venv/bin/activatepython -m pytest tests/tools/test_credential_files.py tests/tools/test_skills_tool.py -qregister_credential_file('../outside.txt')returnsFalseregister_credential_file('/abs/path')returnsFalseHERMES_HOMEpointing outside is rejected and not mountedValidation
python -m pytest tests/tools/test_credential_files.py -q→16 passedpython -m pytest tests/tools/test_skills_tool.py -q→77 passedget_credential_file_mounts()