diff --git a/packages/sandbox/daemon-go/internal/routes/fs.go b/packages/sandbox/daemon-go/internal/routes/fs.go index 2a98f74127..65b3ee9650 100644 --- a/packages/sandbox/daemon-go/internal/routes/fs.go +++ b/packages/sandbox/daemon-go/internal/routes/fs.go @@ -22,6 +22,7 @@ import ( const ( maxImageBytes = 5 * 1024 * 1024 + maxEditFileBytes = 10 * 1024 * 1024 maxTransferBytes = 500 * 1024 * 1024 transferDeadline = 5 * time.Minute ) @@ -419,6 +420,10 @@ func Edit(deps FsDeps) http.HandlerFunc { httpx.Error(w, 400, "old_string and new_string must differ") return } + if stat, err := os.Stat(filePath); err == nil && stat.Size() > maxEditFileBytes { + httpx.Error(w, 400, fmt.Sprintf("File too large (%d bytes; cap is %d)", stat.Size(), maxEditFileBytes)) + return + } raw, err := os.ReadFile(filePath) if err != nil { httpx.Error(w, 400, fmt.Sprintf("File not found: %s", body.Path)) diff --git a/packages/sandbox/daemon-go/internal/routes/fs_edit_test.go b/packages/sandbox/daemon-go/internal/routes/fs_edit_test.go new file mode 100644 index 0000000000..6ae25a4987 --- /dev/null +++ b/packages/sandbox/daemon-go/internal/routes/fs_edit_test.go @@ -0,0 +1,64 @@ +package routes + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +func newEditRequest(t *testing.T, path, oldString, newString string) *http.Request { + t.Helper() + body, err := json.Marshal(map[string]any{ + "path": path, + "old_string": oldString, + "new_string": newString, + }) + if err != nil { + t.Fatalf("marshal body: %v", err) + } + return httptest.NewRequest(http.MethodPost, "/_sandbox/edit", bytes.NewReader(body)) +} + +// TestEditRejectsOversizedFile guards the same untrusted-size boundary as the +// read route: without a cap, editing a large file reads it entirely into +// memory to compute the replacement, which can OOM the daemon. +func TestEditRejectsOversizedFile(t *testing.T) { + repoDir := t.TempDir() + big := strings.Repeat("a", maxEditFileBytes+1) + filePath := filepath.Join(repoDir, "big.txt") + if err := os.WriteFile(filePath, []byte(big), 0o644); err != nil { + t.Fatalf("write fixture: %v", err) + } + + handler := Edit(FsDeps{AppRoot: repoDir, RepoDir: repoDir}) + rec := httptest.NewRecorder() + handler(rec, newEditRequest(t, "big.txt", "a", "b")) + + if rec.Code != 400 { + t.Fatalf("status = %d, want 400; body = %s", rec.Code, rec.Body.String()) + } + if !strings.Contains(rec.Body.String(), "too large") { + t.Fatalf("body = %s, want a too-large error", rec.Body.String()) + } +} + +func TestEditAllowsFileUnderCap(t *testing.T) { + repoDir := t.TempDir() + filePath := filepath.Join(repoDir, "small.txt") + if err := os.WriteFile(filePath, []byte("hello world"), 0o644); err != nil { + t.Fatalf("write fixture: %v", err) + } + + handler := Edit(FsDeps{AppRoot: repoDir, RepoDir: repoDir}) + rec := httptest.NewRecorder() + handler(rec, newEditRequest(t, "small.txt", "hello", "goodbye")) + + if rec.Code != 200 { + t.Fatalf("status = %d, want 200; body = %s", rec.Code, rec.Body.String()) + } +}