security: isolate release issue content from shell execution (#307)
CI / lint (pull_request) Successful in 29s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-08 14:13:56 +00:00
1 parent bd5e66b4e5
commit 7d633e67e8
4 files changed
+111 -23

No files matched your search

+77 -5
View File
@@ -1,4 +1,5 @@
import json
import sys
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
@@ -12,12 +13,34 @@ from src.release_engine import (
Issue,
ReleaseEngine,
RunState,
ShellRunner,
StateStore,
branch_name,
select_issue,
)
def test_shell_runner_treats_metacharacters_as_literal_arguments(tmp_path):
output = tmp_path / "argument.txt"
injected = tmp_path / "injected.txt"
payload = f"release title; touch {injected}"
result = ShellRunner().run(
[
sys.executable,
"-c",
"from pathlib import Path; import sys; Path(sys.argv[1]).write_text(sys.argv[2])",
str(output),
payload,
],
tmp_path,
)
assert result.returncode == 0
assert output.read_text() == payload
assert not injected.exists()
def test_stale_token_error_explains_how_to_restore_cron_authentication():
class UnauthorizedHandler(BaseHTTPRequestHandler):
def do_GET(self):
@@ -70,9 +93,11 @@ class FakeRunner:
def __init__(self, results):
self.results = list(results)
self.commands = []
self.environments = []
def run(self, command, cwd, env=None):
self.commands.append(command)
self.environments.append(env)
return self.results.pop(0)
@@ -91,7 +116,7 @@ def config(tmp_path):
agent="timmy",
repo_path=tmp_path,
state_path=tmp_path / "state.json",
agent_command="agent --issue {issue_number}",
agent_command="agent --issue-from-env",
test_command="pytest -q",
)
@@ -121,6 +146,24 @@ def test_claim_verification_blocks_execution(tmp_path):
assert runner.commands == []
def test_empty_agent_command_fails_before_claim(tmp_path):
issue = Issue(14, "Engine", "body", "open", [], None)
client = FakeClient([issue])
cfg = EngineConfig(
repo="stackchain/stackchain-dashboard",
agent="timmy",
repo_path=tmp_path,
state_path=tmp_path / "state.json",
agent_command=" ",
test_command="pytest -q",
)
with pytest.raises(ValueError, match="invalid agent command: command is empty"):
ReleaseEngine(cfg, client, FakeRunner([])).execute(issue_number=14)
assert client.claimed == []
def test_agent_failure_blocks_tests_and_pr(tmp_path):
issue = Issue(14, "Engine", "body", "open", [], None)
client = FakeClient([issue])
@@ -134,12 +177,41 @@ def test_agent_failure_blocks_tests_and_pr(tmp_path):
engine.execute(issue_number=14)
assert runner.commands == [
"git checkout -B timmy/14-engine",
"agent --issue 14",
["git", "checkout", "-B", "timmy/14-engine"],
["agent", "--issue-from-env"],
]
assert client.created_prs == []
def test_issue_content_reaches_agent_only_through_environment(tmp_path):
title = "$(touch /tmp/title-injected); `id`"
body = "line one\n&& touch /tmp/body-injected > stolen"
issue = Issue(14, title, body, "open", [], None)
client = FakeClient([issue])
runner = FakeRunner([
CommandResult(0, "", ""),
CommandResult(1, "", "agent stopped"),
])
cfg = EngineConfig(
repo="stackchain/stackchain-dashboard",
agent="timmy",
repo_path=tmp_path,
state_path=tmp_path / "state.json",
agent_command="agent --work",
test_command="pytest -q",
)
with pytest.raises(RuntimeError, match="coding agent failed"):
ReleaseEngine(cfg, client, runner).execute(issue_number=14)
assert runner.commands == [
["git", "checkout", "-B", "timmy/14-touch-tmp-title-injected-id"],
["agent", "--work"],
]
assert runner.environments[1]["RELEASE_ISSUE_TITLE"] == title
assert runner.environments[1]["RELEASE_ISSUE_BODY"] == body
def test_failed_tests_block_push_and_pr(tmp_path):
issue = Issue(14, "Engine", "body", "open", [], None)
client = FakeClient([issue])
@@ -153,7 +225,7 @@ def test_failed_tests_block_push_and_pr(tmp_path):
with pytest.raises(RuntimeError, match="tests failed"):
engine.execute(issue_number=14)
assert "git push" not in " ".join(runner.commands)
assert not any(command[:2] == ["git", "push"] for command in runner.commands)
assert client.created_prs == []
@@ -171,7 +243,7 @@ def test_passing_run_pushes_and_opens_linked_pr(tmp_path):
result = engine.execute(issue_number=14)
assert result["pr_number"] == 44
assert runner.commands[-1].startswith("git push -u origin")
assert runner.commands[-1] == ["git", "push", "-u", "origin", "timmy/14-engine-mvp"]
assert len(client.created_prs) == 1
body = client.created_prs[0][4]
assert "Closes #14" in body