Skip to content

bug(api): no request body size limit — unbounded JSON parsing enables memory exhaustion #269

@fireddd

Description

@fireddd

Bug

No HTTP handler uses http.MaxBytesReader to limit request body size. All JSON body parsing goes through json.NewDecoder(r.Body) which reads the entire body into memory without any size constraint. While the daemon is loopback-only (127.0.0.1), a buggy or malicious client can send a multi-gigabyte request body, exhausting daemon memory and crashing the process.

Analyzed against: 96d1649 (current main)
Confidence: High — verified all decodeJSON/decodeJSONStrict call sites.

Affected Endpoints

All endpoints that parse a JSON request body:

  • POST /api/v1/projects (AddProject)
  • PUT /api/v1/projects/{id}/config (SetProjectConfig)
  • POST /api/v1/sessions (Spawn)
  • POST /api/v1/sessions/{sessionId}/send (Send)
  • PATCH /api/v1/sessions/{sessionId} (Rename)
  • POST /api/v1/sessions/{sessionId}/rollback (Rollback)
  • POST /api/v1/prs/{id}/resolve-comments (ResolveComments)
  • POST /api/v1/reviews/execute (TriggerReview)
  • POST /api/v1/reviews/{id}/send (SubmitReview)

The only application-level size check is maxPromptLen = 4096 on the prompt field in Spawn, but a body with megabytes of unknown JSON fields (which decodeJSON ignores) will still be fully read into memory before the prompt check runs.

Root Cause

backend/internal/httpd/controllers/projects.go (and sessions.go, reviews.go, prs.go) all use:

func decodeJSON(r *http.Request, v any) error {
    return json.NewDecoder(r.Body).Decode(v)
}

No http.MaxBytesReader wrapping is applied to r.Body anywhere.

Reproduction

# Generate a 500MB JSON body
python3 -c "print('{\"prompt\": \"x\", \"junk\": \"' + 'A'*500000000 + '\"}')" > /tmp/big.json

# Send it to any endpoint
curl -X POST http://127.0.0.1:3001/api/v1/sessions \
  -H 'Content-Type: application/json' \
  --data-binary @/tmp/big.json
# Daemon memory spikes to ~500MB+ before the request is rejected

Impact

  • Memory exhaustion: Daemon OOM crash from a single request
  • Loopback mitigates but doesn't eliminate risk: Any process on the host can hit the daemon; also, simple POST requests from a web page (Content-Type text/plain) bypass CORS preflight and the body is read before CORS rejection

Suggested Fix

Add a middleware or per-handler http.MaxBytesReader wrapper. A reasonable default for this daemon's payloads would be 1-4 MB:

func limitBody(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        r.Body = http.MaxBytesReader(w, r.Body, 4<<20) // 4 MB
        next.ServeHTTP(w, r)
    })
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions