From ebd7eea08034f9295bb2b9d10f275044e91f5616 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 18 Aug 2026 12:10:32 -0300 Subject: [PATCH] fix(sandbox): cap the Go daemon's /grep result limit A caller-supplied `limit` on /grep had no upper bound, unlike /glob's matching globMaxResultLimit cap. A large limit lets rg's match lines accumulate in memory unbounded, the same crash-the-daemon DoS class #6176 closed for request bodies. --- .../sandbox/daemon-go/internal/routes/fs.go | 10 ++++- .../daemon-go/internal/routes/fs_test.go | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/sandbox/daemon-go/internal/routes/fs.go b/packages/sandbox/daemon-go/internal/routes/fs.go index 3a21c382ae..7b5e69303e 100644 --- a/packages/sandbox/daemon-go/internal/routes/fs.go +++ b/packages/sandbox/daemon-go/internal/routes/fs.go @@ -470,6 +470,11 @@ func Edit(deps FsDeps) http.HandlerFunc { } } +const ( + grepDefaultResultLimit = 250 + grepMaxResultLimit = 10000 +) + func Grep(deps FsDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body struct { @@ -520,9 +525,12 @@ func Grep(deps FsDeps) http.HandlerFunc { } args = append(args, "--", body.Pattern, searchPath) - limit := 250 + limit := grepDefaultResultLimit if body.Limit > 0 { limit = body.Limit + if limit > grepMaxResultLimit { + limit = grepMaxResultLimit + } } cmd := exec.Command("rg", args...) diff --git a/packages/sandbox/daemon-go/internal/routes/fs_test.go b/packages/sandbox/daemon-go/internal/routes/fs_test.go index 30def7d396..33f4a0e029 100644 --- a/packages/sandbox/daemon-go/internal/routes/fs_test.go +++ b/packages/sandbox/daemon-go/internal/routes/fs_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -58,6 +59,44 @@ func TestDecodeBodyRejectsOversizedRequest(t *testing.T) { } } +// Without an upper bound, a caller-supplied `limit` let /grep buffer an +// unbounded number of match lines into memory — the same crash-the-daemon +// class the request-body cap above closes for the request side. +func TestGrepCapsCallerSuppliedLimit(t *testing.T) { + if _, err := exec.LookPath("rg"); err != nil { + t.Skip("ripgrep not installed") + } + repoDir := t.TempDir() + var content strings.Builder + for i := 0; i < grepMaxResultLimit+500; i++ { + content.WriteString("needle\n") + } + if err := os.WriteFile(filepath.Join(repoDir, "haystack.txt"), []byte(content.String()), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + deps := FsDeps{AppRoot: repoDir, RepoDir: repoDir} + body, _ := json.Marshal(map[string]any{ + "pattern": "needle", + "output_mode": "content", + "limit": grepMaxResultLimit * 10, + }) + req := httptest.NewRequest(http.MethodPost, "/grep", bytes.NewReader(body)) + rec := httptest.NewRecorder() + Grep(deps)(rec, req) + if rec.Code != 200 { + t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + var out struct { + MatchCount int `json:"matchCount"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if out.MatchCount > grepMaxResultLimit { + t.Fatalf("expected matchCount capped at %d, got %d", grepMaxResultLimit, out.MatchCount) + } +} + func readReq(t *testing.T, deps FsDeps, path string) *httptest.ResponseRecorder { t.Helper() body, _ := json.Marshal(map[string]any{"path": path, "full": true})