diff --git a/cmd/review-bot/main.go b/cmd/review-bot/main.go index 1ee0380..1150453 100644 --- a/cmd/review-bot/main.go +++ b/cmd/review-bot/main.go @@ -173,6 +173,14 @@ func main() { os.Exit(1) } + // Early validation of filesystem-path flags (fail fast before network I/O) + if *docMapFile != "" { + if _, err := validateWorkspacePath(*docMapFile, "doc-map"); err != nil { + slog.Error("invalid doc-map path", "error", err) + os.Exit(1) + } + } + // Initialize clients // Detect VCS type: explicit flag > env var > URL heuristic (default: gitea). vcsType := envOrDefault("VCS_TYPE", "") diff --git a/cmd/review-bot/main_test.go b/cmd/review-bot/main_test.go index 22b7a6c..c7a3675 100644 --- a/cmd/review-bot/main_test.go +++ b/cmd/review-bot/main_test.go @@ -1506,3 +1506,77 @@ func TestMainSubprocess_DeprecatedGiteaURLEnv(t *testing.T) { t.Errorf("expected deprecation warning for GITEA_URL, got: %s", out) } } + +// TestMainSubprocess_InvalidDocMapPath confirms that --doc-map with a path traversal +// attempt is rejected before any network I/O. +func TestMainSubprocess_InvalidDocMapPath(t *testing.T) { + if os.Getenv("TEST_SUBPROCESS_MAIN") == "1" { + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) + os.Args = []string{"review-bot", + "--vcs-url", "https://gitea.example.com", + "--repo", "owner/repo", + "--pr", "1", + "--reviewer-token", "tok", + "--llm-base-url", "https://api.example.com", + "--llm-api-key", "key", + "--llm-model", "gpt-4", + "--doc-map", "../../../etc/passwd", + } + main() + return + } + + cmd := exec.Command(os.Args[0], "-test.run=TestMainSubprocess_InvalidDocMapPath") + cmd.Env = append(cleanEnv(), + "TEST_SUBPROCESS_MAIN=1", + "GITHUB_WORKSPACE="+t.TempDir(), + ) + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected non-zero exit with path traversal doc-map, got success") + } + output := string(out) + if !strings.Contains(output, "doc-map") { + t.Errorf("expected error mentioning doc-map, got: %s", output) + } + if !strings.Contains(output, "resolves outside workspace") { + t.Errorf("expected error about path traversal, got: %s", output) + } +} + +// TestMainSubprocess_InvalidDocMapFile confirms that --doc-map with a nonexistent file +// is rejected before any network I/O. +func TestMainSubprocess_InvalidDocMapFile(t *testing.T) { + if os.Getenv("TEST_SUBPROCESS_MAIN") == "1" { + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) + os.Args = []string{"review-bot", + "--vcs-url", "https://gitea.example.com", + "--repo", "owner/repo", + "--pr", "1", + "--reviewer-token", "tok", + "--llm-base-url", "https://api.example.com", + "--llm-api-key", "key", + "--llm-model", "gpt-4", + "--doc-map", "nonexistent.yml", + } + main() + return + } + + cmd := exec.Command(os.Args[0], "-test.run=TestMainSubprocess_InvalidDocMapFile") + cmd.Env = append(cleanEnv(), + "TEST_SUBPROCESS_MAIN=1", + "GITHUB_WORKSPACE="+t.TempDir(), + ) + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected non-zero exit with nonexistent doc-map file, got success") + } + output := string(out) + if !strings.Contains(output, "doc-map") { + t.Errorf("expected error mentioning doc-map, got: %s", output) + } + if !strings.Contains(output, "failed to resolve") { + t.Errorf("expected error about failed resolution, got: %s", output) + } +}