Skip to content

Chunked request#2

Merged
MitchBradley merged 18 commits into
mainfrom
ChunkedRequest
Mar 30, 2026
Merged

Chunked request#2
MitchBradley merged 18 commits into
mainfrom
ChunkedRequest

Conversation

@MitchBradley
Copy link
Copy Markdown
Owner

No description provided.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/WebRequest.cpp
Comment on lines +376 to +377

if (_handler) {
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When handling a chunked body, handleBody is invoked with total set to _contentLength, which will be 0 for Transfer-Encoding: chunked requests that do not also send Content-Length or X-Expected-Entity-Length. Handlers such as AsyncCallbackJsonWebHandler::handleBody in src/AsyncJson.cpp assume total reflects the full body size and allocate/copy based on it; with total == 0, they allocate a 1-byte buffer and then write beyond it as index and len grow, causing a buffer overflow. To avoid this, either do not route truly length-unknown chunked bodies through handlers that rely on total (e.g., reject such requests or require X-Expected-Entity-Length), or change the contract/implementation so that chunked requests provide a safe upper bound and handlers guard against total == 0.

Suggested change
if (_handler) {
//
// For truly length-unknown chunked bodies (_contentLength == 0),
// some handlers assume that the "total" argument reflects the full
// body size and allocate buffers based on it. To avoid passing a
// misleading total of 0 together with non-zero data (which can
// cause buffer overflows in such handlers), we only forward data
// when the total length is known, or when this is the final
// zero-length chunk used to signal end-of-body.
if (_handler && (_contentLength != 0 || curLen == 0)) {

Copilot uses AI. Check for mistakes.
Comment thread examples/ChunkRequest/ChunkRequest.ino Outdated
Comment on lines +115 to +117
state = new RequestState{File()};
request->_tempObject = static_cast<void *>(state);

Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequestState is allocated with new and stored in request->_tempObject, but AsyncWebServerRequest::~AsyncWebServerRequest in src/WebRequest.cpp:96-123 calls free(_tempObject) on any non-null pointer. If a request is destroyed (e.g., due to client disconnect or abort) before handleRequest runs and deletes state/nulls out _tempObject, this will invoke free on memory allocated with new, which is undefined behavior. To make this robust, either use malloc/calloc so that free is always correct, or update the core destructor / life-cycle to use delete (and ensure _tempObject is cleared on all paths).

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +140
String path = request->url();

state = new RequestState{File()};
request->_tempObject = static_cast<void *>(state);

if (total) {
#ifdef ESP32
size_t avail = LittleFS.totalBytes() - LittleFS.usedBytes();
#else
FSInfo info;
LittleFS.info(info);
auto avail = info.totalBytes - info.usedBytes;
#endif
avail -= 4096; // Reserve a block for overhead
if (total > avail) {
Serial.printf("PUT %u bytes will not fit in available space (%u).\n", total, avail);
request->send(507, "text/plain", "Too large for available storage\r\n");
return;
}
}
Serial.print("Opening ");
Serial.print(path);
Serial.println(" from handleBody");
#ifdef ESP32
File file = LittleFS.open(path, "w", true);
#else
File file = LittleFS.open(path, "w");
#endif
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In handleBody, chunked PUT requests open and write to LittleFS files using the raw request->url() path without any access control or path validation. A remote client can stream arbitrary content into any writable path on the filesystem, overwriting existing files or placing malicious data. Introduce authentication and path restrictions here as well, ensuring only authorized clients and a controlled set of filenames can be used for uploads.

Copilot uses AI. Check for mistakes.
WiFi.softAPConfig(local_IP, gateway, subnet);

WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WiFi.softAP("esp-captive") configures an open WiFi access point with no password, so anyone in radio range can connect to the network hosting this file-upload server. This makes the unauthenticated PUT endpoints trivially reachable by arbitrary nearby attackers, who can then modify the device filesystem. Use WPA2 (or stronger) credentials for the AP in any non-lab environment or disable the AP and expose the service only on a secured network segment.

Suggested change
WiFi.softAP("esp-captive");
// Configure a WPA2-protected access point instead of an open AP
WiFi.softAP("esp-captive", "ChangeThisPW123");

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +105
String path = request->url();

// This PUT code executes if the body was empty, which
// can happen if the client creates a zero-length file.
// MacOS WebDAVFS does that, then later LOCKs the file
// and issues a subsequent PUT with body contents.

#ifdef ESP32
File file = LittleFS.open(path, "w", true);
#else
File file = LittleFS.open(path, "w");
#endif

if (file) {
file.close();
request->send(201); // Created
return;
}
request->send(403);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleRequest creates LittleFS files directly from request->url() on any HTTP PUT without any authentication or authorization. An attacker who can reach the device (e.g., over the ESP access point) can create or overwrite arbitrary files on the filesystem by choosing the path in the URL. Add authentication/authorization before opening files and constrain or validate the allowed paths so only expected locations can be written.

Copilot uses AI. Check for mistakes.
MitchBradley and others added 2 commits January 29, 2026 15:01
Copilot AI review requested due to automatic review settings January 30, 2026 15:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/WebRequest.cpp
// marking the end of the data stream.

if (_handler) {
_handler->handleBody(this, buf + i, curLen, _chunkStartIndex, _contentLength);
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the chunked body path, handleBody is called with total set to _contentLength (_handler->handleBody(this, buf + i, curLen, _chunkStartIndex, _contentLength);). For chunked requests that do not send Content-Length or X-Expected-Entity-Length, _contentLength remains 0 even though there is a non-empty body, but some existing handlers (e.g. AsyncCallbackJsonWebHandler::handleBody in src/AsyncJson.cpp) assume total is the actual body length and allocate a buffer of size total + 1, which will be far too small and will be overrun as index grows. To avoid this memory corruption, either ensure that such handlers never see chunked bodies with total == 0 (e.g. by rejecting these requests or requiring X-Expected-Entity-Length), or change the interface/handlers so they can safely handle "unknown total length" without using total for fixed-size allocation.

Suggested change
_handler->handleBody(this, buf + i, curLen, _chunkStartIndex, _contentLength);
// For chunked requests that do not specify Content-Length or
// X-Expected-Entity-Length, _contentLength remains zero even
// though there may be a non-empty body. Some handlers assume
// that the "total" argument is the full body length and use
// it for fixed-size allocations; passing total == 0 together
// with non-zero data length can therefore lead to buffer
// overruns as more chunks arrive. To avoid this, do not call
// handleBody with non-empty data when _contentLength is zero.
if (!(_contentLength == 0 && curLen > 0)) {
_handler->handleBody(this, buf + i, curLen, _chunkStartIndex, _contentLength);
}

Copilot uses AI. Check for mistakes.
Comment thread src/literals.h
DECLARE_STR(T_HTTP_CODE_503, "Service Unavailable");
DECLARE_STR(T_HTTP_CODE_504, "Gateway Time-out");
DECLARE_STR(T_HTTP_CODE_505, "HTTP Version Not Supported");
DECLARE_STR(T_HTTP_CODE_507, "Insufficient Storage");
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added T_HTTP_CODE_507 here, but AsyncWebServerResponse::responseCodeToString in src/WebResponses.cpp does not yet have a case 507: that returns this string. As a result, request->send(507, ...) will still produce the generic reason phrase (T_HTTP_CODE_ANY / "Unknown code"), so to complete 507 support you should add a 507 case to that switch to use T_HTTP_CODE_507.

Copilot uses AI. Check for mistakes.
Comment thread examples/ChunkRequest/ChunkRequest.ino Outdated
request->_tempObject = std::malloc(sizeof(RequestState));

// Use placement new to construct the RequestState object therein
RequestState *state = new (request->_tempObject) RequestState{File()};
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In handleBody, the state pointer is initialized from request->_tempObject at the top of the function, but inside the if (index == 0) block you redeclare a new RequestState *state for the placement-new, which shadows the outer variable. As a result, after the block, the if (state && state->outFile) check uses the original (likely null) pointer, so the first body fragment (where index == 0) is never written to the file and the initial bytes of a PUT request are silently dropped. To fix this, avoid shadowing: construct into request->_tempObject and then assign the resulting pointer back to the existing state variable (or use a different local name) so that the same pointer is used for the subsequent write logic.

Suggested change
RequestState *state = new (request->_tempObject) RequestState{File()};
state = new (request->_tempObject) RequestState{File()};

Copilot uses AI. Check for mistakes.
Comment thread examples/ChunkRequest/ChunkRequest.ino Outdated
LittleFS.info(info);
auto avail = info.totalBytes - info.usedBytes;
#endif
avail -= 4096; // Reserve a block for overhead
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The available-space check subtracts a fixed 4096-byte reserve (avail -= 4096) before comparing total > avail, but avail is an unsigned type. When the filesystem has less than 4096 bytes free, this subtraction underflows and avail becomes a very large value, so oversized uploads will not be rejected early as intended. Consider guarding the reserve subtraction (if (avail <= 4096) { avail = 0; } else { avail -= 4096; }) or doing the comparison in a way that cannot underflow.

Suggested change
avail -= 4096; // Reserve a block for overhead
if (avail <= 4096) {
avail = 0; // Not enough space to reserve a block for overhead
} else {
avail -= 4096; // Reserve a block for overhead
}

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 31, 2026 21:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +206 to +226
#if ASYNCWEBSERVER_WIFI_SUPPORTED
#define AP_SUBNET 100
IPAddress local_IP(192, 168, AP_SUBNET, 1);
IPAddress gateway(192, 168, AP_SUBNET, 1);
IPAddress subnet(255, 255, 255, 0);
WiFi.softAPConfig(local_IP, gateway, subnet);

WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif

#ifdef ESP32
LittleFS.begin(true);
#else
LittleFS.begin();
#endif

server.onRequestBody(handleBody);
server.onNotFound(handleRequest);

server.begin();
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example exposes an unauthenticated file upload endpoint over an open Wi-Fi access point, allowing any nearby client to arbitrarily write to the LittleFS filesystem. With WiFi.softAP("esp-captive") creating an open AP and server.onRequestBody(handleBody)/server.onNotFound(handleRequest) accepting HTTP_PUT without any authentication or authorization, an attacker within RF range can upload or overwrite files on the device. Restrict access by securing the AP (e.g., WPA2 with a strong passphrase) and/or enforcing authentication and authorization checks before handling PUT requests, or clearly gating this behavior to non-production/debug builds only.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 8, 2026 08:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings February 10, 2026 10:45
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@MitchBradley MitchBradley merged commit 0d10b08 into main Mar 30, 2026
36 of 38 checks passed
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.

3 participants