@@ -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