mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-12 10:17:28 +00:00
`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>
56 lines
1.8 KiB
Go
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())
|
|
}
|
|
})
|
|
}
|