"""Tests for agent_id path-traversal guard in keypair._key_path. agent_id is appended verbatim to the PEM filename: ~/.muse/keys/{hostname}__{agent_id}.pem Without sanitization, a malicious or buggy agent_id like ``../../.bashrc`` resolves outside ~/.muse/keys/ via pathlib, allowing writes to arbitrary filesystem locations. The fix: reject any agent_id containing path separators or characters that would escape the keys directory. Coverage -------- I Path traversal attempts are rejected I1 agent_id with ../ is rejected by _key_path I2 agent_id with leading / is rejected I3 agent_id with backslash is rejected I4 agent_id with null byte is rejected II Safe agent_ids are accepted II1 alphanumeric handle passes II2 handle with hyphens and underscores passes II3 handle with dots (not leading, not traversal) passes III End-to-end: keygen with malicious agent_id exits non-zero III1 muse auth keygen --agent-id ../../evil exits non-zero """ from __future__ import annotations import pathlib import pytest from muse.core import keypair as kp_module from muse.core import identity as id_module from tests.cli_test_helper import CliRunner runner = CliRunner() _HUB = "http://localhost:10003" _MNEMONIC = ( "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon about" ) @pytest.fixture() def isolated(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path: fake_home = tmp_path / "home" fake_home.mkdir(parents=True, exist_ok=True) monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home)) monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys") monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse") monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml") return fake_home @pytest.fixture() def fixed_mnemonic(monkeypatch: pytest.MonkeyPatch) -> str: from muse.core import bip39 as bip39_mod monkeypatch.setattr(bip39_mod, "generate_mnemonic", lambda **kw: _MNEMONIC) return _MNEMONIC # --------------------------------------------------------------------------- # I Path traversal attempts are rejected by _key_path # --------------------------------------------------------------------------- class TestKeyPathTraversalRejected: def test_I1_dotdot_slash_rejected(self) -> None: """I1: agent_id containing ../ must raise ValueError.""" with pytest.raises((ValueError, OSError)): kp_module._key_path("localhost:10003", "../../.bashrc") def test_I2_leading_slash_rejected(self) -> None: """I2: agent_id with a leading / must raise ValueError.""" with pytest.raises((ValueError, OSError)): kp_module._key_path("localhost:10003", "/etc/passwd") def test_I3_backslash_rejected(self) -> None: """I3: agent_id with a backslash must raise ValueError.""" with pytest.raises((ValueError, OSError)): kp_module._key_path("localhost:10003", "evil\\agent") def test_I4_null_byte_rejected(self) -> None: """I4: agent_id with a null byte must raise ValueError.""" with pytest.raises((ValueError, OSError)): kp_module._key_path("localhost:10003", "evil\x00agent") # --------------------------------------------------------------------------- # II Safe agent_ids are accepted # --------------------------------------------------------------------------- class TestKeyPathSafeAgentId: def test_II1_alphanumeric_accepted(self) -> None: """II1: plain alphanumeric handle is accepted.""" path = kp_module._key_path("localhost:10003", "myagent123") assert "myagent123" in path.name def test_II2_hyphens_underscores_accepted(self) -> None: """II2: hyphens and underscores are safe.""" path = kp_module._key_path("localhost:10003", "my-agent_v2") assert "my-agent_v2" in path.name def test_II3_internal_dot_accepted(self) -> None: """II3: an internal dot (not traversal) is safe.""" path = kp_module._key_path("localhost:10003", "agent.v2") assert "agent.v2" in path.name # --------------------------------------------------------------------------- # III End-to-end: keygen with malicious agent_id exits non-zero # --------------------------------------------------------------------------- class TestKeygenTraversalEndToEnd: def test_III1_keygen_traversal_agent_id_rejected( self, isolated: pathlib.Path, fixed_mnemonic: str ) -> None: """III1: muse auth keygen --agent-id ../../evil exits non-zero.""" # First establish operator identity runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--json"]) r = runner.invoke( None, ["auth", "keygen", "--hub", _HUB, "--agent-id", "../../evil", "--json"], ) assert r.exit_code != 0, ( f"keygen with traversal agent_id must be rejected, got exit 0:\n{r.output}" )