test_injection.py
python
sha256:8d7f41aafae41deee70035a92f93602ef1722a290153597451e5932af503a42c
docs: queue board-identity follow-ups so they survive the session
Human
11 hours ago
| 1 | """Security tests — injection surfaces in adapter inputs.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import pytest |
| 6 | |
| 7 | from adapters.errors import ConfigError, ReadError, WriteError |
| 8 | from tests.support import adapter_for, make_runner, ok |
| 9 | |
| 10 | |
| 11 | def test_commit_feature_rejects_path_traversal(git_only_config, repo_root) -> None: |
| 12 | adapter = adapter_for(git_only_config, repo_root, make_runner({})) |
| 13 | result = adapter.commit_feature( |
| 14 | branch="feat/x", |
| 15 | message="ok", |
| 16 | paths=["../../etc/passwd"], |
| 17 | ) |
| 18 | assert isinstance(result, ReadError) |
| 19 | |
| 20 | |
| 21 | def test_commit_feature_rejects_dash_prefixed_path(muse_only_config, repo_root) -> None: |
| 22 | adapter = adapter_for(muse_only_config, repo_root, make_runner({})) |
| 23 | result = adapter.commit_feature( |
| 24 | branch="feat/x", |
| 25 | message="ok", |
| 26 | paths=["-rf"], |
| 27 | ) |
| 28 | assert isinstance(result, ReadError) |
| 29 | |
| 30 | |
| 31 | def test_commit_feature_refuses_protected_main_across_regimes( |
| 32 | git_only_config, |
| 33 | muse_only_config, |
| 34 | muse_git_mirror_config, |
| 35 | repo_root, |
| 36 | ) -> None: |
| 37 | for config in (git_only_config, muse_only_config, muse_git_mirror_config): |
| 38 | adapter = adapter_for(config, repo_root, make_runner({})) |
| 39 | result = adapter.commit_feature(branch="main", message="x", paths=[]) |
| 40 | assert isinstance(result, WriteError) |
| 41 | |
| 42 | |
| 43 | def test_shell_metacharacters_are_quoted(git_only_config, repo_root) -> None: |
| 44 | runner = make_runner( |
| 45 | { |
| 46 | "git checkout": ok(""), |
| 47 | "git add": ok(""), |
| 48 | "git commit": ok(""), |
| 49 | "git rev-parse": ok("deadbeef"), |
| 50 | } |
| 51 | ) |
| 52 | adapter = adapter_for(git_only_config, repo_root, runner) |
| 53 | branch = "feat/$(rm -rf /)" |
| 54 | adapter.commit_feature(branch=branch, message="safe", paths=["docs/a.md"]) |
| 55 | checkout_cmds = [c[0] for c in runner.calls if "git checkout" in c[0]] |
| 56 | assert checkout_cmds == [f"git checkout 'feat/$(rm -rf /)'"] |
| 57 | |
| 58 | |
| 59 | def test_template_substitution_does_not_execute_shell(git_only_config) -> None: |
| 60 | from adapters.templating import build_token_map, substitute_tokens |
| 61 | |
| 62 | token_map = build_token_map(git_only_config) |
| 63 | payload = "{{repo.name}}; $(rm -rf /); `whoami`" |
| 64 | result = substitute_tokens(payload, token_map) |
| 65 | assert "$(rm -rf /)" in result |
| 66 | assert result.startswith("test-git") |
| 67 | |
| 68 | |
| 69 | def test_template_rejects_unknown_token_keys() -> None: |
| 70 | from adapters.templating import substitute_tokens |
| 71 | |
| 72 | with pytest.raises(ConfigError, match="unknown or unmapped"): |
| 73 | substitute_tokens("{{not.in.registry}}", {"repo.name": "x"}) |
| 74 |
File History
1 commit
sha256:8d7f41aafae41deee70035a92f93602ef1722a290153597451e5932af503a42c
docs: queue board-identity follow-ups so they survive the session
Human
11 hours ago