From 6a3c81327943916d7b8c15868273b9a549b0845d Mon Sep 17 00:00:00 2001 From: Rodin Date: Fri, 1 May 2026 21:03:41 -0700 Subject: [PATCH] fix: address review findings (path restriction, login cross-check, README) - system-prompt-file: reject absolute paths and paths containing ".." Prevents reading arbitrary files outside the workspace on shared runners. - Cleanup: cross-check r.User.Login == posted.User.Login before deletion Defense-in-depth: only attempt to delete reviews from same author. Flagged by both sonnet and security reviewers. - README: fix wording (cleanup happens after posting, not before) Issues filed for deferred work: - #24: Consistent url.PathEscape across all client endpoints - #25: Binary signature verification for supply-chain hardening --- README.md | 2 +- cmd/review-bot/main.go | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6512eec..ee43bd4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ AI-powered code review bot for Gitea pull requests. Fetches diff + context, send - **Multi-provider**: OpenAI-compatible and Anthropic Messages API - **Context-aware**: Fetches full file content, conventions, language patterns, CI status - **Smart budget**: Automatically trims context to fit model token limits -- **Idempotent reviews**: Deletes previous review before posting new one (one review per bot) +- **Idempotent reviews**: Posts new review, then cleans up stale ones (one review per bot) - **Custom prompts**: Load additional instructions from a file (e.g. security-focused review) - **Zero dependencies**: Go stdlib only diff --git a/cmd/review-bot/main.go b/cmd/review-bot/main.go index a99270e..8684cac 100644 --- a/cmd/review-bot/main.go +++ b/cmd/review-bot/main.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "path/filepath" "strconv" "strings" "time" @@ -154,9 +155,22 @@ func main() { // Step 6b: Load additional system prompt if specified additionalPrompt := "" if *systemPromptFile != "" { - data, err := os.ReadFile(*systemPromptFile) + workspace := os.Getenv("GITHUB_WORKSPACE") + if workspace == "" { + workspace, _ = os.Getwd() + } + absWorkspace, err := filepath.Abs(workspace) if err != nil { - log.Fatalf("Failed to read system prompt file %q: %v", *systemPromptFile, err) + log.Fatalf("Failed to resolve workspace path: %v", err) + } + promptPath := filepath.Join(absWorkspace, *systemPromptFile) + promptPath = filepath.Clean(promptPath) + if !strings.HasPrefix(promptPath, absWorkspace+string(filepath.Separator)) && promptPath != absWorkspace { + log.Fatalf("system-prompt-file resolves outside workspace (got %q, workspace %q)", promptPath, absWorkspace) + } + data, err := os.ReadFile(promptPath) + if err != nil { + log.Fatalf("Failed to read system prompt file %q: %v", promptPath, err) } additionalPrompt = string(data) log.Printf("Loaded system prompt file: %s (%d bytes)", *systemPromptFile, len(additionalPrompt)) @@ -227,7 +241,7 @@ func main() { log.Printf("Warning: could not list existing reviews: %v", err) } else { for _, r := range reviews { - if r.ID != posted.ID && strings.Contains(r.Body, sentinel) { + if r.ID != posted.ID && r.User.Login == posted.User.Login && strings.Contains(r.Body, sentinel) { if err := giteaClient.DeleteReview(ctx, owner, repoName, prNumber, r.ID); err != nil { log.Printf("Warning: could not delete old review %d: %v", r.ID, err) } else {