Files
graphify/tests/test_skillgen_input_path_injection.py
T
ayushcodes10 43026db66f Add regression tests executing Step 1 with a hostile substituted path
Extracts the real Step 1 bash block from the committed skill.md
artifact, substitutes INPUT_PATH the way an agent following the
instructions would, and actually runs it, so the fix is checked
against what users receive rather than the fragment source alone. One
test confirms a command substitution embedded in the substituted path
never runs, another confirms a semicolon separated command never
runs, a third confirms a legitimate path with a space in it still
resolves correctly, and a fourth confirms a nonexistent path still
fails the whole step loudly. All four were checked against the
pre fix block first, three failed as expected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit ad98afa9f5)
2026-09-25 16:01:53 +01:00

108 lines
3.8 KiB
Python

"""#3642: Step 1's INPUT_PATH substitution into a bash command is a command
injection vector.
INPUT_PATH is a literal placeholder in every generated skill file, meant to
be substituted by the agent following the instructions with the resolved
scan path before it runs the bash block. A malicious or merely
untrusted-source path substituted into `echo "$(cd INPUT_PATH && pwd)" ...`
executed as shell code the moment Step 1 ran, before any Python code was
ever reached -- the outer double quotes do not stop `$()`/backtick
expansion, and the bare `cd INPUT_PATH` was itself unquoted.
These tests extract the actual Step 1 bash block from a committed,
generated skill file, substitute INPUT_PATH the way an agent would, and
execute it for real, so the fix is verified against the artifact users
actually receive rather than against the fragment source alone.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
SKILL_MD = REPO_ROOT / "graphify" / "skill.md"
def _extract_step1_bash_block() -> str:
text = SKILL_MD.read_text(encoding="utf-8")
for match in re.finditer(r"```bash\n(.*?)\n```", text, re.DOTALL):
block = match.group(1)
if "graphify_root" in block:
return block
raise AssertionError("could not find the Step 1 bash block in skill.md")
@pytest.fixture()
def step1_script() -> str:
return _extract_step1_bash_block()
def _run_step1(script: str, input_path_value: str, cwd: Path) -> subprocess.CompletedProcess:
substituted = script.replace("INPUT_PATH", input_path_value)
return subprocess.run(
["bash", "-c", substituted],
cwd=cwd, capture_output=True, text=True,
env={**os.environ, "PATH": os.environ.get("PATH", "")},
)
def test_step1_does_not_execute_a_command_substitution_in_input_path(tmp_path):
"""A malicious path containing $(...) must never run as shell code."""
script = _extract_step1_bash_block()
sentinel = tmp_path / "PWNED"
malicious = f"$(touch {sentinel})"
_run_step1(script, malicious, cwd=tmp_path)
assert not sentinel.exists(), (
"a $(...) command substitution inside the substituted INPUT_PATH "
"must never execute"
)
def test_step1_does_not_execute_a_semicolon_separated_command_in_input_path(tmp_path):
"""A malicious path using `;` to chain a second command must never run."""
script = _extract_step1_bash_block()
sentinel = tmp_path / "PWNED2"
malicious = f"nonexistent; touch {sentinel} #"
_run_step1(script, malicious, cwd=tmp_path)
assert not sentinel.exists(), (
"a semicolon-separated command inside the substituted INPUT_PATH "
"must never execute"
)
def test_step1_still_resolves_a_legitimate_path(tmp_path):
"""The fix must not break the ordinary, non-malicious case."""
script = _extract_step1_bash_block()
project = tmp_path / "my project"
project.mkdir()
result = _run_step1(script, str(project), cwd=tmp_path)
marker = tmp_path / "graphify-out" / ".graphify_root"
assert marker.exists(), (
f"a legitimate path must still be resolved and written; "
f"stdout={result.stdout!r} stderr={result.stderr!r}"
)
assert marker.read_text(encoding="utf-8") == str(project.resolve())
def test_step1_still_fails_loudly_on_a_nonexistent_path(tmp_path):
"""A path that does not exist must still fail, matching the original
`cd INPUT_PATH` behavior, not silently write a bogus marker."""
script = _extract_step1_bash_block()
result = _run_step1(script, "does/not/exist", cwd=tmp_path)
marker = tmp_path / "graphify-out" / ".graphify_root"
assert result.returncode != 0
assert not marker.exists() or marker.read_text(encoding="utf-8") == ""