From 11683c7564f28df3ff29b80e166fa62d591d7855 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 18 Aug 2026 11:33:22 -0300 Subject: [PATCH] fix(sandbox): cap the Go daemon's fs route request bodies decodeBody (used by every /fs/* route: read, write, edit, mkdir, unlink, mv, list, write_from_url, upload_to_url) read the request body with an unbounded io.ReadAll. A caller sending an oversized body could buffer it entirely into the daemon's memory and crash it, tearing down the sandbox pod on the next missed health probe. Cap it at maxTransferBytes (500MB), the same bound the daemon already uses for its other file-transfer path, following the same LimitReader pattern used to cap the JSON-RPC response body in #6162. Test: TestDecodeBodyRejectsOversizedRequest drives decodeBody with an infinite reader and asserts it rejects instead of buffering forever. --- .../sandbox/daemon-go/internal/routes/fs.go | 9 +++++- .../daemon-go/internal/routes/fs_test.go | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/sandbox/daemon-go/internal/routes/fs.go b/packages/sandbox/daemon-go/internal/routes/fs.go index 2a98f74127..296fe40376 100644 --- a/packages/sandbox/daemon-go/internal/routes/fs.go +++ b/packages/sandbox/daemon-go/internal/routes/fs.go @@ -42,11 +42,18 @@ func (d FsDeps) notifyWrite(path string) { } } +// decodeBody caps the request body at maxTransferBytes: without a limit, a +// misbehaving or malicious caller could stream an unbounded body into memory +// and crash the daemon, tearing down the sandbox pod on the next missed +// health probe. The cap matches the daemon's other file-transfer bound. func decodeBody(r *http.Request, out any) error { - raw, err := io.ReadAll(r.Body) + raw, err := io.ReadAll(io.LimitReader(r.Body, maxTransferBytes+1)) if err != nil { return fmt.Errorf("Failed to parse body: %s", err.Error()) } + if len(raw) > maxTransferBytes { + return fmt.Errorf("Request body exceeded %d bytes", maxTransferBytes) + } if err := json.Unmarshal(raw, out); err != nil { return fmt.Errorf("Failed to parse body: %s", err.Error()) } diff --git a/packages/sandbox/daemon-go/internal/routes/fs_test.go b/packages/sandbox/daemon-go/internal/routes/fs_test.go index 54ea7ea284..f1b0708537 100644 --- a/packages/sandbox/daemon-go/internal/routes/fs_test.go +++ b/packages/sandbox/daemon-go/internal/routes/fs_test.go @@ -3,10 +3,12 @@ package routes import ( "bytes" "encoding/json" + "io" "net/http" "net/http/httptest" "os" "path/filepath" + "strings" "testing" ) @@ -30,6 +32,32 @@ func seedBlocks(t *testing.T) FsDeps { return FsDeps{AppRoot: filepath.Dir(repoDir), RepoDir: repoDir} } +// infiniteReader never hits EOF; it lets a test drive decodeBody's cap +// without pre-allocating the oversized body it's rejecting. +type infiniteReader struct{} + +func (infiniteReader) Read(p []byte) (int, error) { + for i := range p { + p[i] = 'a' + } + return len(p), nil +} + +// Without a cap, decodeBody's io.ReadAll(r.Body) would buffer an unbounded +// request body into memory and could crash the daemon, tearing down the +// sandbox pod on the next missed health probe. +func TestDecodeBodyRejectsOversizedRequest(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/write", io.NopCloser(infiniteReader{})) + var out map[string]any + err := decodeBody(req, &out) + if err == nil { + t.Fatal("expected decodeBody to reject an oversized body, got nil error") + } + if !strings.Contains(err.Error(), "exceeded") { + t.Fatalf("expected a size-limit error, got: %v", err) + } +} + func readReq(t *testing.T, deps FsDeps, path string) *httptest.ResponseRecorder { t.Helper() body, _ := json.Marshal(map[string]any{"path": path, "full": true})