mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(profiles): validate tar archive member paths on import
Fixes a zip-slip path traversal vulnerability in hermes profile import. shutil.unpack_archive() on untrusted tar members allows entries like ../../escape.txt to write files outside ~/.hermes/profiles/. - Add _normalize_profile_archive_parts() to reject absolute paths (POSIX and Windows), traversal (..), empty paths, backslash tricks - Add _safe_extract_profile_archive() for manual per-member extraction that only allows regular files and directories (rejects symlinks) - Replace shutil.unpack_archive() with the safe extraction path - Add regression tests for traversal and absolute-path attacks Co-authored-by: Gutslabs <gutslabsxyz@gmail.com>
This commit is contained in:
parent
08171c1c31
commit
0f2ea2062b
2 changed files with 100 additions and 4 deletions
|
|
@ -6,6 +6,7 @@ and shell completion generation.
|
|||
"""
|
||||
|
||||
import json
|
||||
import io
|
||||
import os
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
|
@ -449,6 +450,40 @@ class TestExportImport:
|
|||
with pytest.raises(FileExistsError):
|
||||
import_profile(str(archive_path), name="coder")
|
||||
|
||||
def test_import_rejects_traversal_archive_member(self, profile_env, tmp_path):
|
||||
archive_path = tmp_path / "export" / "evil.tar.gz"
|
||||
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
escape_path = tmp_path / "escape.txt"
|
||||
|
||||
with tarfile.open(archive_path, "w:gz") as tf:
|
||||
info = tarfile.TarInfo("../../escape.txt")
|
||||
data = b"pwned"
|
||||
info.size = len(data)
|
||||
tf.addfile(info, io.BytesIO(data))
|
||||
|
||||
with pytest.raises(ValueError, match="Unsafe archive member path"):
|
||||
import_profile(str(archive_path), name="coder")
|
||||
|
||||
assert not escape_path.exists()
|
||||
assert not get_profile_dir("coder").exists()
|
||||
|
||||
def test_import_rejects_absolute_archive_member(self, profile_env, tmp_path):
|
||||
archive_path = tmp_path / "export" / "evil-abs.tar.gz"
|
||||
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
absolute_target = tmp_path / "abs-escape.txt"
|
||||
|
||||
with tarfile.open(archive_path, "w:gz") as tf:
|
||||
info = tarfile.TarInfo(str(absolute_target))
|
||||
data = b"pwned"
|
||||
info.size = len(data)
|
||||
tf.addfile(info, io.BytesIO(data))
|
||||
|
||||
with pytest.raises(ValueError, match="Unsafe archive member path"):
|
||||
import_profile(str(archive_path), name="coder")
|
||||
|
||||
assert not absolute_target.exists()
|
||||
assert not get_profile_dir("coder").exists()
|
||||
|
||||
def test_export_nonexistent_raises(self, profile_env, tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
export_profile("nonexistent", str(tmp_path / "out.tar.gz"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue