mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 07:59:40 +00:00
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)
108 lines
3.8 KiB
Python
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") == ""
|