mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 10:03:29 +00:00
baa1f75847
* fix(server): cap JSON body + header size (TASK-663) decodeJSON called json.NewDecoder(r.Body).Decode(v) with no size limit. Any client could POST a multi-GB JSON blob and watch Pad stream the whole thing into one allocation — a single request could OOM the process. - internal/server/server.go: wrap r.Body in http.MaxBytesReader(..., 2 MB) inside decodeJSON. Every legitimate payload (item, collection, auth, etc.) is well under 100 KB so 2 MB is several orders of magnitude above real traffic. Factor out decodeJSONWithLimit(maxBytes) so future bulk-import endpoints can opt in to a larger cap without removing the wrapper. - internal/server/server.go: set MaxHeaderBytes = 64 KiB on the http.Server (default is 1 MB). Plenty for cookies/auth/CORS while cheaply rejecting header-flood DoS. Test: decode_json_test.go covers the 3 MiB body rejection, a happy path, and a custom-limit override that rejects a 1 MiB body under a 256 KiB cap. Parent: PLAN-643 (OSS Security Hardening). * fix(server): bump workspace import JSON cap to 64 MiB per Codex P1 Codex flagged that handleImportWorkspace inherits the new 2 MiB default cap, but WorkspaceExport contains full collections, items, comments, and item_versions for the workspace — a realistic project backup routinely exceeds 2 MiB, so existing exports stop re-importing. Switch to decodeJSONWithLimit(64 << 20). 64 MiB is multiple orders of magnitude above any realistic single-workspace backup while still far from heap-exhaustion territory.
64 lines
2.1 KiB
Go
64 lines
2.1 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestDecodeJSON_RejectsOversizeBody ensures the default 2 MiB cap is
|
|
// enforced — without http.MaxBytesReader a multi-GB POST would stream
|
|
// into a single allocation and could OOM the process.
|
|
func TestDecodeJSON_RejectsOversizeBody(t *testing.T) {
|
|
// 3 MiB of harmless but oversize JSON.
|
|
body := []byte(`{"x":"` + strings.Repeat("a", 3<<20) + `"}`)
|
|
req := httptest.NewRequest("POST", "/", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
var target map[string]any
|
|
err := decodeJSON(req, &target)
|
|
if err == nil {
|
|
t.Fatalf("expected oversize body to be rejected, got nil error")
|
|
}
|
|
if !strings.Contains(err.Error(), "request body too large") &&
|
|
!strings.Contains(err.Error(), "http: request body too large") {
|
|
// MaxBytesReader wraps the "request body too large" error into the
|
|
// json.Decoder failure, which bubbles up through the invalid-JSON
|
|
// wrap. Just verify SOME error surfaced — the exact wording is
|
|
// tied to stdlib internals.
|
|
t.Logf("got error: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestDecodeJSON_AcceptsWithinLimit confirms the happy path still works.
|
|
func TestDecodeJSON_AcceptsWithinLimit(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/", bytes.NewReader([]byte(`{"name":"ok"}`)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
var target struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := decodeJSON(req, &target); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if target.Name != "ok" {
|
|
t.Fatalf("got name=%q, want %q", target.Name, "ok")
|
|
}
|
|
}
|
|
|
|
// TestDecodeJSONWithLimit_CustomCap verifies callers can opt in to a
|
|
// larger cap (for bulk-import style endpoints).
|
|
func TestDecodeJSONWithLimit_CustomCap(t *testing.T) {
|
|
// 1 MiB body; below default 2 MiB but above our custom 256 KiB cap.
|
|
body := []byte(`{"x":"` + strings.Repeat("a", 1<<20) + `"}`)
|
|
req := httptest.NewRequest("POST", "/", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
var target map[string]any
|
|
if err := decodeJSONWithLimit(req, &target, 256<<10); err == nil {
|
|
t.Fatal("expected 256 KiB cap to reject 1 MiB body, got nil")
|
|
}
|
|
}
|
|
|