Skip to content

fix(security): TTS output_path traversal + update ZIP symlink reject (salvage #6693 + #15881) - #32056

Merged
teknium1 merged 3 commits into
mainfrom
hermes/hermes-f9dd4507
May 25, 2026
Merged

fix(security): TTS output_path traversal + update ZIP symlink reject (salvage #6693 + #15881)#32056
teknium1 merged 3 commits into
mainfrom
hermes/hermes-f9dd4507

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Two narrow salvages from S04 symlink/path-traversal cluster.

Commits (rebased, contributor authorship preserved)

Salvage scope notes

#6693 originally bundled three concerns. Salvaged TTS only. Dropped:

  • agent/skill_commands.py rglob symlink rejection: would have broken the
    documented design supporting ~/.hermes/skills/<name> as a symlink to a
    checked-out skill elsewhere (comment at skill_commands.py:73-75).
  • tools/delegate_tool.py batch prefix display: pure UX, doesn't belong
    in a security PR.

#6693's original TTS guard confined output to DEFAULT_OUTPUT_DIR-or-cwd,
which broke 9 existing tests writing to tmp_path locations. Narrowed to
traversal-component-only.

#15881 originally bundled four concerns. Salvaged ZIP symlink only. Dropped:

  • config.yaml write-deny: already on main via is_write_denied.
  • Quick commands danger detection: contradicts explicit cli.py comment
    that quick_commands are user-defined shell snippets from config.yaml,
    not agent/LLM controlled.
  • Memory plugin shlex.split for dep checks: already on main.

Test plan

pytest tests/tools/test_tts_path_traversal.py                    # 4 passed
pytest tests/hermes_cli/test_update_zip_symlink_reject.py        # 2 passed
pytest tests/tools/ tests/hermes_cli/ -k 'tts or update or zip'  # 460 passed

Co-authored-by: aaronlab 1115117931@qq.com
Co-authored-by: codeblackhole1024 lichriszhang@gmail.com

Closes #6693
Closes #15881

aaronlab and others added 3 commits May 25, 2026 05:15
text_to_speech_tool accepts an explicit output_path. Without a traversal
guard, a path containing '..' components (whether prompt-injection-
controlled, from a confused skill, or just a buggy caller) could escape
its declared base and write the audio to a system location — e.g.
`output_path='audio/../../etc/cron.d/x'` lands the file outside the
intended audio cache.

Reject '..' components in the user-supplied path. Explicit absolute
paths are unchanged (the agent legitimately writes audio wherever the
user/caller asks); only traversal-style escapes are blocked. The
terminal tool can still write anywhere with approval — this just keeps
the unattended TTS surface from materializing files via traversal.

Regression tests cover: '..' in the middle (audio/../../etc/...),
bare '..' prefix, and the negative cases (absolute paths + relative
paths without '..' both pass through unchanged).

Salvaged from PR #6693 by @aaronlab. The original PR confined output to
DEFAULT_OUTPUT_DIR-or-cwd, which broke 9 existing tests that legitimately
write to tmp_path locations. The traversal-only check covers the actual
threat (path-escape via '..' from prompt injection) without restricting
where users can choose to write their audio.

The remaining pieces of #6693 (skill_commands rglob symlink rejection,
delegate_tool batch prefix display) are dropped:
- skill_commands rglob: breaks the documented design supporting
  ~/.hermes/skills/<name> as a symlink to a checked-out skill elsewhere
  (see comment at agent/skill_commands.py:73-75)
- delegate_tool batch prefix: pure UX, doesn't belong in a security PR

Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
_update_via_zip downloads a source ZIP from GitHub and calls
zipfile.ZipFile.extractall. The existing zip-slip path guard validates
each member's path stays under tmp_dir, but does not check member type
— so a ZIP containing a symlink member would still be materialized by
extractall, and a symlink target could point outside the extracted
tree (or to a sensitive system path).

This isn't a high-likelihood threat for hermes-agent's actual GitHub
source ZIPs (we don't ship symlinks), but the extractall path runs as
the user's account and a compromised mirror could plant arbitrary files
via the symlink → target → write chain.

Reject any member whose Unix mode bits (upper 16 bits of external_attr)
are S_IFLNK before extractall. Hermes source ZIPs contain only regular
files and directories; a symlink member is unambiguously suspicious.

Regression tests cover: symlink member rejection (raises ValueError,
caught by the outer try/except as a clean SystemExit, no extraction),
and the happy-path verification that a normal ZIP doesn't trigger the
symlink reject message.

Salvaged from PR #15881 by @codeblackhole1024. The remaining pieces of
that PR were already on main or contradicted explicit design decisions:
- config.yaml write-deny: already in agent/file_safety.py's
  control_file_names denylist (the modern guard); the proposed addition
  to build_write_denied_paths was the legacy path.
- Quick commands danger detection: contradicts the explicit
  cli.py:8491-8492 comment 'shell=True is intentional: quick_commands
  are user-defined shell snippets from config.yaml — not agent/LLM
  controlled.'
- Memory plugin shlex.split for dep checks: already on main
  (hermes_cli/memory_setup.py:133).

Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
@teknium1
teknium1 merged commit e9119e0 into main May 25, 2026
20 of 22 checks passed
@teknium1
teknium1 deleted the hermes/hermes-f9dd4507 branch May 25, 2026 12:15
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-f9dd4507 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: 9320 on HEAD, 9319 on base (🆕 +1)

🆕 New issues (1):

Rule Count
unresolved-import 1
First entries
tests/hermes_cli/test_update_zip_symlink_reject.py:17: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues: none

Unchanged: 4930 pre-existing issues carried over.

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

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround tool/tts Text-to-speech and transcription comp/cli CLI entry point, hermes_cli/, setup wizard labels May 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P1 High — major feature broken, no workaround tool/tts Text-to-speech and transcription type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants