gabriel / muse public
test_agent_id_traversal.py python
128 lines 5.0 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
1 """Tests for agent_id path-traversal guard in keypair._key_path.
2
3 agent_id is appended verbatim to the PEM filename:
4 ~/.muse/keys/{hostname}__{agent_id}.pem
5
6 Without sanitization, a malicious or buggy agent_id like ``../../.bashrc``
7 resolves outside ~/.muse/keys/ via pathlib, allowing writes to arbitrary
8 filesystem locations. The fix: reject any agent_id containing path
9 separators or characters that would escape the keys directory.
10
11 Coverage
12 --------
13 I Path traversal attempts are rejected
14 I1 agent_id with ../ is rejected by _key_path
15 I2 agent_id with leading / is rejected
16 I3 agent_id with backslash is rejected
17 I4 agent_id with null byte is rejected
18
19 II Safe agent_ids are accepted
20 II1 alphanumeric handle passes
21 II2 handle with hyphens and underscores passes
22 II3 handle with dots (not leading, not traversal) passes
23
24 III End-to-end: keygen with malicious agent_id exits non-zero
25 III1 muse auth keygen --agent-id ../../evil exits non-zero
26 """
27
28 from __future__ import annotations
29
30 import pathlib
31
32 import pytest
33
34 from muse.core import keypair as kp_module
35 from muse.core import identity as id_module
36 from tests.cli_test_helper import CliRunner
37
38 runner = CliRunner()
39 _HUB = "http://localhost:10003"
40 _MNEMONIC = (
41 "abandon abandon abandon abandon abandon abandon abandon abandon "
42 "abandon abandon abandon about"
43 )
44
45
46 @pytest.fixture()
47 def isolated(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
48 fake_home = tmp_path / "home"
49 fake_home.mkdir(parents=True, exist_ok=True)
50 monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home))
51 monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys")
52 monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse")
53 monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml")
54 return fake_home
55
56
57 @pytest.fixture()
58 def fixed_mnemonic(monkeypatch: pytest.MonkeyPatch) -> str:
59 from muse.core import bip39 as bip39_mod
60 monkeypatch.setattr(bip39_mod, "generate_mnemonic", lambda **kw: _MNEMONIC)
61 return _MNEMONIC
62
63
64 # ---------------------------------------------------------------------------
65 # I Path traversal attempts are rejected by _key_path
66 # ---------------------------------------------------------------------------
67
68 class TestKeyPathTraversalRejected:
69 def test_I1_dotdot_slash_rejected(self) -> None:
70 """I1: agent_id containing ../ must raise ValueError."""
71 with pytest.raises((ValueError, OSError)):
72 kp_module._key_path("localhost:10003", "../../.bashrc")
73
74 def test_I2_leading_slash_rejected(self) -> None:
75 """I2: agent_id with a leading / must raise ValueError."""
76 with pytest.raises((ValueError, OSError)):
77 kp_module._key_path("localhost:10003", "/etc/passwd")
78
79 def test_I3_backslash_rejected(self) -> None:
80 """I3: agent_id with a backslash must raise ValueError."""
81 with pytest.raises((ValueError, OSError)):
82 kp_module._key_path("localhost:10003", "evil\\agent")
83
84 def test_I4_null_byte_rejected(self) -> None:
85 """I4: agent_id with a null byte must raise ValueError."""
86 with pytest.raises((ValueError, OSError)):
87 kp_module._key_path("localhost:10003", "evil\x00agent")
88
89
90 # ---------------------------------------------------------------------------
91 # II Safe agent_ids are accepted
92 # ---------------------------------------------------------------------------
93
94 class TestKeyPathSafeAgentId:
95 def test_II1_alphanumeric_accepted(self) -> None:
96 """II1: plain alphanumeric handle is accepted."""
97 path = kp_module._key_path("localhost:10003", "myagent123")
98 assert "myagent123" in path.name
99
100 def test_II2_hyphens_underscores_accepted(self) -> None:
101 """II2: hyphens and underscores are safe."""
102 path = kp_module._key_path("localhost:10003", "my-agent_v2")
103 assert "my-agent_v2" in path.name
104
105 def test_II3_internal_dot_accepted(self) -> None:
106 """II3: an internal dot (not traversal) is safe."""
107 path = kp_module._key_path("localhost:10003", "agent.v2")
108 assert "agent.v2" in path.name
109
110
111 # ---------------------------------------------------------------------------
112 # III End-to-end: keygen with malicious agent_id exits non-zero
113 # ---------------------------------------------------------------------------
114
115 class TestKeygenTraversalEndToEnd:
116 def test_III1_keygen_traversal_agent_id_rejected(
117 self, isolated: pathlib.Path, fixed_mnemonic: str
118 ) -> None:
119 """III1: muse auth keygen --agent-id ../../evil exits non-zero."""
120 # First establish operator identity
121 runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--json"])
122 r = runner.invoke(
123 None,
124 ["auth", "keygen", "--hub", _HUB, "--agent-id", "../../evil", "--json"],
125 )
126 assert r.exit_code != 0, (
127 f"keygen with traversal agent_id must be rejected, got exit 0:\n{r.output}"
128 )
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago