From 2050053d8affd21361b13fba6f769b592f92f71b Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Mon, 24 Aug 2026 16:24:34 -0300 Subject: [PATCH] fix(sandbox): cap the Go daemon's /tools/sync request body --- .../daemon-go/internal/routes/tools.go | 9 ++++- .../daemon-go/internal/routes/tools_test.go | 35 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 packages/sandbox/daemon-go/internal/routes/tools_test.go diff --git a/packages/sandbox/daemon-go/internal/routes/tools.go b/packages/sandbox/daemon-go/internal/routes/tools.go index 409d3357f2..b388d4b075 100644 --- a/packages/sandbox/daemon-go/internal/routes/tools.go +++ b/packages/sandbox/daemon-go/internal/routes/tools.go @@ -20,6 +20,13 @@ type toolsSyncBody struct { ExpiresAt *float64 `json:"expiresAt"` } +// maxToolsSyncBodyBytes bounds the /tools/sync request body: a URL plus a +// handful of headers, never a file transfer, so 1MB is generous headroom. +// Without a limit, json.Decoder streams an unbounded body into memory and +// could crash the daemon, tearing down the sandbox pod on the next missed +// health probe. +const maxToolsSyncBodyBytes = 1024 * 1024 + // ToolsSync handles POST /_sandbox/tools/sync — body `{ url, headers, // expiresAt? }` (the run's Virtual MCP endpoint). Writes the endpoint file, // then lists the endpoint's tools and writes a JSON Schema catalog under @@ -29,7 +36,7 @@ type toolsSyncBody struct { func ToolsSync(deps ToolsDeps) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body toolsSyncBody - raw := json.NewDecoder(r.Body) + raw := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxToolsSyncBodyBytes)) if err := raw.Decode(&body); err != nil { httpx.Error(w, 400, "invalid JSON body") return diff --git a/packages/sandbox/daemon-go/internal/routes/tools_test.go b/packages/sandbox/daemon-go/internal/routes/tools_test.go new file mode 100644 index 0000000000..b23e92d716 --- /dev/null +++ b/packages/sandbox/daemon-go/internal/routes/tools_test.go @@ -0,0 +1,35 @@ +package routes + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// Without a cap, ToolsSync's json.Decoder would stream an unbounded request +// body into memory and could crash the daemon, tearing down the sandbox pod +// on the next missed health probe. +func TestToolsSyncRejectsOversizedBody(t *testing.T) { + oversized := `{"url":"https://example.com","headers":{"pad":"` + + strings.Repeat("a", maxToolsSyncBodyBytes) + `"}}` + req := httptest.NewRequest(http.MethodPost, "/_sandbox/tools/sync", strings.NewReader(oversized)) + rec := httptest.NewRecorder() + + ToolsSync(ToolsDeps{})(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want 400; body = %s", rec.Code, rec.Body.String()) + } +} + +func TestToolsSyncRejectsMissingURL(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/_sandbox/tools/sync", strings.NewReader(`{"headers":{}}`)) + rec := httptest.NewRecorder() + + ToolsSync(ToolsDeps{})(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want 400; body = %s", rec.Code, rec.Body.String()) + } +}