Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/sandbox/daemon-go/internal/routes/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions packages/sandbox/daemon-go/internal/routes/tools_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
Loading