Files
meshcore-analyzer/cmd/server/body_limits_test.go
T
nullrouten0andClaude Mythos 5.1 0a6870c508 fix(api): cap request bodies on /api/decode and /api/packets/observations (#2119)
`POST /api/decode` and `POST /api/packets/observations` are
unauthenticated and decoded the request body with no size limit.
`json.Decoder` buffers the whole value in memory, and `/api/decode`
hex-decodes the string before the 184-byte payload check runs. On a test
instance one 100 MB request raised RSS by about 180 MB. Many requests in
parallel can push a container past its memory limit, and a restart
reloads the dataset for several minutes.

**Fix:** `http.MaxBytesReader` on both handlers, the same way the
geo-filter PUT and `/api/paths/inspect` already do it.

| endpoint | cap | why |
|---|---|---|
| `/api/decode` | 4 KB | a packet is at most ~512 hex chars |
| `/api/packets/observations` | 64 KB | 200 hashes of 64 chars is ~14 KB
|

Oversized bodies get HTTP 413 instead of a generic 400.

**Tests:** `body_limits_test.go` — normal bodies still return 200,
oversized bodies return 413, for both endpoints. Full `go test ./...` in
`cmd/server` passes.

**Note:** deployments behind a reverse proxy with its own body limit
were already safe. The shipped Caddy config has no body limit, so
default installs were not.

Running in production on our instance since 2026-10-07.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
2026-10-08 16:26:10 +02:00

56 lines
1.8 KiB
Go

package main
import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// The two unauthenticated POST endpoints must cap their request bodies.
// Before the cap, json.Decoder buffered the whole body and /api/decode
// hex-decoded it before the 184-byte payload check ran, so one request
// could hold a multi-hundred-MB buffer.
func TestUnauthenticatedPostBodyLimits(t *testing.T) {
_, router := setupTestServerWithAPIKey(t, "")
post := func(path, body string) *httptest.ResponseRecorder {
req := httptest.NewRequest("POST", path, bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
t.Run("decode: normal body still works", func(t *testing.T) {
w := post("/api/decode", `{"hex":"0200"}`)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String())
}
})
t.Run("decode: oversized body is rejected with 413", func(t *testing.T) {
big := `{"hex":"` + strings.Repeat("ab", decodeBodyLimit) + `"}`
w := post("/api/decode", big)
if w.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("expected 413, got %d (body: %s)", w.Code, w.Body.String())
}
})
t.Run("observations: normal body still works", func(t *testing.T) {
w := post("/api/packets/observations", `{"hashes":["abc123"]}`)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String())
}
})
t.Run("observations: oversized body is rejected with 413", func(t *testing.T) {
big := `{"hashes":["` + strings.Repeat("a", batchObservationsBodyLimit) + `"]}`
w := post("/api/packets/observations", big)
if w.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("expected 413, got %d (body: %s)", w.Code, w.Body.String())
}
})
}