Skip to content

Add 8KB body size limit to JSON POST endpoints to prevent heap exhaustion#798

Open
blade-running-man wants to merge 1 commit intoBlueforcer:mainfrom
blade-running-man:fix/post-body-size-limit
Open

Add 8KB body size limit to JSON POST endpoints to prevent heap exhaustion#798
blade-running-man wants to merge 1 commit intoBlueforcer:mainfrom
blade-running-man:fix/post-body-size-limit

Conversation

@blade-running-man
Copy link

Problem

POST endpoints that parse JSON (/api/notify, /api/custom, /api/indicator1-3, etc.) have no payload size limit. Sending a large payload causes ArduinoJson to allocate unbounded memory via DynamicJsonDocument, exhausting the ESP32 heap. After a few oversized requests, even normal-sized requests start failing with 500 ErrorParsingJson because there is not enough free heap left.

Root cause

The WebServer reads the full request body into arg("plain") regardless of size, then each handler passes it directly to ArduinoJson for parsing. ArduinoJson allocates roughly 1.3x the input size for its internal document. On an ESP32 with ~200KB free heap, a single 32KB payload can consume a significant portion of available memory, and the fragmentation left behind can prevent subsequent allocations from succeeding.

Fix

Add a static isBodyOversized() helper that checks arg("plain").length() > 8192 before any JSON parsing. If the body exceeds 8KB, the handler immediately returns 400 Bad Request without allocating a DynamicJsonDocument.

The 8KB limit matches the existing MQTT_MAX_PACKET_SIZE (defined in platformio.ini), so HTTP and MQTT have the same maximum payload size.

Guarded endpoints (parse JSON):

/api/moodlight, /api/notify, /api/apps (POST), /api/settings (POST), /api/reorder, /api/custom, /api/indicator1, /api/indicator2, /api/indicator3

Not guarded (plain-text / small payloads):

/api/power, /api/sleep, /api/rtttl, /api/sound, /api/r2d2, /api/switch

How to reproduce

Test script

IP="your_ip"

# 1. Send 10KB payload to /api/notify
python3 -c "import json; print(json.dumps({'text': 'A'*10000}))" | \
  curl -s -o /dev/null -w "10KB /api/notify:    HTTP %{http_code}\n" \
  -X POST http://$IP/api/notify -d @-

# 2. Send 10KB payload to /api/custom
python3 -c "import json; print(json.dumps({'text': 'A'*10000}))" | \
  curl -s -o /dev/null -w "10KB /api/custom:    HTTP %{http_code}\n" \
  -X POST "http://$IP/api/custom?name=test" -d @-

# 3. Now try a normal request
curl -s -o /dev/null -w "Normal /api/notify:   HTTP %{http_code}\n" \
  -X POST http://$IP/api/notify -d '{"text":"hello","duration":3}'

Before (main branch, firmware 0.98)

10KB /api/notify:    HTTP 500    ← ArduinoJson fails on large input
10KB /api/custom:    HTTP 200    ← accepted, consumes heap
Normal /api/notify:  HTTP 500    ← subsequent normal request ALSO fails (heap exhausted)

After sending oversized payloads, the device enters a degraded state where even valid requests fail until reboot.

After (this branch)

10KB /api/notify:    HTTP 400    ← rejected before parsing
10KB /api/custom:    HTTP 400    ← rejected before parsing
Normal /api/notify:  HTTP 200    ← works fine, heap is clean

POST endpoints that parse JSON bodies have no size limit, allowing
a single request to allocate unbounded memory via ArduinoJson and
potentially cause an OOM crash on the ESP32.

Add isBodyOversized() guard that rejects payloads over 8KB (matching
MQTT_MAX_PACKET_SIZE) with 400 Bad Request. Applied to endpoints that
parse JSON: /api/moodlight, /api/notify, /api/apps, /api/settings,
/api/reorder, /api/custom, /api/indicator1-3.

Not applied to /api/power, /api/rtttl, /api/sound, /api/r2d2 which
accept small plain-text or URL-encoded payloads.

Reproduce: curl -X POST http://<ip>/api/notify -d @large_file.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant