Skip to content

fix(security): protect Hermes control-plane files from prompt injection (#14157)#30397

Merged
teknium1 merged 3 commits into
mainfrom
hermes/hermes-37ccebe4
May 22, 2026
Merged

fix(security): protect Hermes control-plane files from prompt injection (#14157)#30397
teknium1 merged 3 commits into
mainfrom
hermes/hermes-37ccebe4

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Salvages @PratikRai0101's PR #14157. write_file and patch can no longer be used to rewrite Hermes' control-plane files (auth.json, config.yaml, webhook_subscriptions.json, mcp-tokens/) via prompt injection — the deny list catches them at both the active profile view AND the global root view.

Root cause

is_write_denied() covered shell rc files, .ssh, .env, etc. but did not cover Hermes' own control plane. A prompt-injected write_file(~/.hermes/auth.json, ...) succeeded silently — no terminal-approval prompt, no deny — letting the model rewrite OAuth state, providers, routing, or webhook handlers persistently across sessions.

Changes

  • agent/file_safety.py (+23, contributor): adds active HERMES_HOME control files + mcp-tokens/ to the deny set. Uses realpath so traversal (../) and symlinks resolve correctly.
  • tests/tools/test_file_operations.py (+44, contributor): parameterized coverage — direct paths, traversal attempts, allowed standard paths.
  • agent/file_safety.py (+27 -19, follow-up): widen to BOTH _hermes_home_path() AND _hermes_root_path(). Without this, profile mode (HERMES_HOME = <root>/profiles/<name>) left <root>/auth.json + <root>/config.yaml writable — same shape as the .env gap PR write_file tool bypasses credential protection for global ~/.hermes/.env #15981 closed. Also tighten the mcp-tokens/ check from startswith(dir + sep) to == dir OR startswith(dir + sep) so writing the directory entry itself is blocked.
  • tests/tools/test_file_operations.py (+36, follow-up): regression tests use a real profile-mode layout (<tmp>/hermes/profiles/coder as HERMES_HOME, <tmp>/hermes as root) and assert protection on both views.

Stripped from the contributor diff

The original PR included ~50 lines of unrelated black-style formatter reflows in test_file_operations.py — blank-line insertions, line-break tweaks on unchanged code. Stripped during salvage so the contributor commit reflects only the security work. Also stripped two cosmetic edits in agent/file_safety.py (an inserted blank line and one line-break on safe_root).

Validation

Scenario Before After
write_file(~/.hermes/auth.json) succeeds denied
write_file(~/.hermes/mcp-tokens/gh.json) succeeds denied
write_file(~/.hermes/dummy/../auth.json) succeeds denied (realpath)
Profile mode: write_file(~/.hermes/auth.json) (gap, contributor PR alone) denied
Profile mode: write_file(~/.hermes/profiles/coder/auth.json) succeeds denied
write_file(/tmp/anything.txt) succeeds succeeds

tests/tools/test_file_operations.py — 69/69 pass (61 original + 3 contributor + 5 follow-up).
tests/tools/test_write_deny.py (PR #15981 regression suite) — 20/20 pass (no overlap conflict).

Attribution

@PratikRai0101's commit preserved with original authorship (date 2026-04-23) — rebase-merge keeps it visible in git log. Follow-up widening commit attributed to Teknium. Closes #14072 + #14157 on merge.

Infographic

PR 14157 control-plane write deny

PratikRai0101 and others added 3 commits May 22, 2026 04:27
Adds active-HERMES_HOME control-plane files to the write deny list:
auth.json, config.yaml, webhook_subscriptions.json, and any path
under mcp-tokens/. realpath() resolves before comparison so
directory-traversal and symlink targets are normalised, preventing
trivial deny-list bypass via ../ tricks.

Without this, a prompt-injected agent could rewrite Hermes' own
auth state or routing config via write_file / patch — without
triggering the terminal dangerous-command approval — and persist
attacker-controlled behaviour across sessions.

Fixes #14072
PR #14157 added control-plane write-deny against the ACTIVE HERMES_HOME,
which is fine in non-profile mode but leaves a gap once a profile is
active: HERMES_HOME points at <root>/profiles/<name>, so the global
<root>/auth.json + <root>/config.yaml + <root>/webhook_subscriptions.json
+ <root>/mcp-tokens/ remain writable. Same shape as the .env gap PR
#15981 closed via _hermes_root_path().

Apply the same widening pattern here. The control-file/mcp-tokens check
now iterates BOTH _hermes_home_path() and _hermes_root_path() (dedupes
when they coincide in non-profile mode). Also tightens the mcp-tokens
check from "startswith dir + os.sep" to "==dir OR startswith dir + os.sep"
so writing the directory entry itself is blocked, not just files inside.

Regression tests cover both protections in a real profile-mode layout
(<tmp>/hermes/profiles/coder as HERMES_HOME, <tmp>/hermes as root).
@teknium1 teknium1 merged commit 1e71b71 into main May 22, 2026
17 checks passed
@teknium1 teknium1 deleted the hermes/hermes-37ccebe4 branch May 22, 2026 11:32
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-37ccebe4 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9017 on HEAD, 9017 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 4766 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent loop, run_agent.py, prompt builder P1 High — major feature broken, no workaround type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: write_file/patch can modify ~/.hermes control-plane files (auth.json, config.yaml, webhook subscriptions)

3 participants