Skip to content

Commit d18ee41

Browse files
etoyamaclaude
andcommitted
fix(#108): launcher.sh -- validate --approved-by against injection
Add regex guard (^[0-9]{8}_[0-9]{6}$) before any user-controlled value reaches inline Python blocks, preventing shell/Python injection via crafted token_id strings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c88592d commit d18ee41

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

skills/batch-analysis/launcher.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ while [[ $# -gt 0 ]]; do
4747
esac
4848
done
4949

50+
# ---------------------------------------------------------------------------
51+
# S1 guard: validate --approved-by format before any interpolation
52+
# ---------------------------------------------------------------------------
53+
if [ -n "$APPROVED_BY" ]; then
54+
if ! [[ "$APPROVED_BY" =~ ^[0-9]{8}_[0-9]{6}$ ]]; then
55+
echo "Invalid --approved-by format: expected YYYYMMDD_HHMMSS, got '$APPROVED_BY'" >&2
56+
exit 1
57+
fi
58+
fi
59+
5060
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5161
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
5262
cd "$PROJECT_ROOT"

tests/integration/test_batch_launcher.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,3 +683,61 @@ def test_phase_b_warns_not_emitted_with_token(
683683
insight_base_dir=insight_root,
684684
)
685685
assert "Phase A transitional" not in result.stderr
686+
687+
688+
# =========================================================================
689+
# S1: --approved-by shell injection validation
690+
# =========================================================================
691+
692+
693+
class TestBatchApprovedByInjection:
694+
"""S1: launcher.sh must reject malformed --approved-by tokens."""
695+
696+
def test_launcher_rejects_malformed_approved_by_token(
697+
self,
698+
insight_root: Path,
699+
config_review_a: Path,
700+
stub_claude_env: None,
701+
) -> None:
702+
"""Malicious shell command in --approved-by must be rejected
703+
by format validation BEFORE reaching any Python interpreter."""
704+
result = run_launcher(
705+
["--approved-by", "evil; rm -rf /"],
706+
cwd=insight_root.parent,
707+
insight_base_dir=insight_root,
708+
)
709+
assert result.returncode == 1
710+
# Must be caught by format guard, not by downstream token lookup
711+
stderr_lower = result.stderr.lower()
712+
assert "invalid --approved-by format" in stderr_lower
713+
714+
def test_launcher_rejects_approved_by_with_quotes(
715+
self,
716+
insight_root: Path,
717+
config_review_a: Path,
718+
stub_claude_env: None,
719+
) -> None:
720+
"""Python injection via quote escape must exit 1."""
721+
result = run_launcher(
722+
["--approved-by", "a'; __import__('os'); #"],
723+
cwd=insight_root.parent,
724+
insight_base_dir=insight_root,
725+
)
726+
assert result.returncode == 1
727+
728+
def test_launcher_accepts_valid_token_format(
729+
self,
730+
insight_root: Path,
731+
config_review_a: Path,
732+
stub_claude_env: None,
733+
) -> None:
734+
"""Well-formed token_id passes format validation."""
735+
token_id = create_valid_token(insight_root, token_id="20260421_080000")
736+
result = run_launcher(
737+
["--approved-by", token_id],
738+
cwd=insight_root.parent,
739+
insight_base_dir=insight_root,
740+
)
741+
# Should not fail due to format validation (may fail for other reasons
742+
# but must NOT exit 1 with "invalid format" message)
743+
assert "invalid" not in result.stderr.lower() or result.returncode == 0

0 commit comments

Comments
 (0)