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})