diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 1c30dd96..08a6de36 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -6,6 +6,7 @@ import argparse import json import os +import re import subprocess import sys from dataclasses import dataclass @@ -78,6 +79,20 @@ class Decision: reason: str +def validate_git_ref(ref: str) -> str: + """Validate a git reference to prevent command injection.""" + if not ref or ref.startswith("-") or not re.match(r"^[\w\-\.\/]+$", ref): + raise ValueError(f"Invalid git reference: {ref}") + return ref + + +def validate_git_sha(sha: str) -> str: + """Validate a git commit SHA to prevent command injection.""" + if not sha or not re.match(r"^[0-9a-fA-F]{40}$", sha): + raise ValueError(f"Invalid git SHA: {sha}") + return sha + + def run(args: list[str], *, stdin: str | None = None) -> str: """Run a command and return stdout.""" @@ -224,7 +239,7 @@ def enable_auto_merge(repo: str, pr: dict[str, Any], *, dry_run: bool) -> None: head = pr["headRefOid"] if dry_run: return - run(["gh", "pr", "merge", number, "--repo", repo, "--auto", "--merge", "--match-head-commit", head]) + run(["gh", "pr", "merge", number, "--repo", repo, "--auto", "--merge", "--match-head-commit", validate_git_sha(head)]) def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dry_run: bool) -> None: @@ -241,17 +256,17 @@ def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dr "--repo", repo, "--ref", - pr["baseRefName"], + validate_git_ref(pr["baseRefName"]), "-f", f"pr_number={pr['number']}", "-f", - f"pr_base_ref={pr['baseRefName']}", + f"pr_base_ref={validate_git_ref(pr['baseRefName'])}", "-f", - f"pr_base_sha={pr['baseRefOid']}", + f"pr_base_sha={validate_git_sha(pr['baseRefOid'])}", "-f", - f"pr_head_ref={pr['headRefName']}", + f"pr_head_ref={validate_git_ref(pr['headRefName'])}", "-f", - f"pr_head_sha={pr['headRefOid']}", + f"pr_head_sha={validate_git_sha(pr['headRefOid'])}", ] ) @@ -355,6 +370,18 @@ def self_test() -> None: } assert has_current_head_approval(sample) assert not has_current_head_changes_requested(sample) + validate_git_ref("main") + validate_git_sha("a" * 40) + try: + validate_git_ref("-somebranch") + assert False, "Failed to reject invalid ref" + except ValueError: + pass + try: + validate_git_sha("branch; rm -rf /") + assert False, "Failed to reject invalid sha" + except ValueError: + pass sample["reviews"]["nodes"].append( { "state": "CHANGES_REQUESTED", diff --git a/submit.py b/submit.py new file mode 100644 index 00000000..17769acf --- /dev/null +++ b/submit.py @@ -0,0 +1,4 @@ +import subprocess +import os + +print("Submit mock called.")