-
Notifications
You must be signed in to change notification settings - Fork 91
Fix #384: Remove the buffer overflow intended by design allowing users to add a null terminator after the buffer end #385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -586,7 +586,6 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const auto datalast = datalen ? data[datalen] : 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (_pinfo.masked) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (size_t i = 0; i < datalen; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -606,7 +605,31 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (datalen > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, data, datalen); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ------------------------------------------------------------ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Issue 384: https://github.com/ESP32Async/ESPAsyncWebServer/issues/384 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Discussion: https://github.com/ESP32Async/ESPAsyncWebServer/pull/383#discussion_r2760425739 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The initial design of the library was doing a backup of the byte following the data buffer because the client code | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // was allowed and documented to do something like data[len] = 0; to facilitate null-terminated string handling. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This was a bit hacky but it was working and it was documented, although completely incorrect because it was modifying a byte outside of the data buffer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // So to fix this behavior and to avoid breaking existing client code that may be relying on this behavior, we now have to copy the data to a temporary buffer that has an extra byte for the null terminator. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ------------------------------------------------------------ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uint8_t *copy = (uint8_t *)malloc(datalen + 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (copy == NULL) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async_ws_log_e("Failed to allocate"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _status = WS_DISCONNECTED; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (_client) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _client->abort(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| memcpy(copy, data, datalen); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| copy[datalen] = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, copy, datalen); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| free(copy); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+615
to
+632
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ------------------------------------------------------------ | |
| uint8_t *copy = (uint8_t *)malloc(datalen + 1); | |
| if (copy == NULL) { | |
| async_ws_log_e("Failed to allocate"); | |
| _status = WS_DISCONNECTED; | |
| if (_client) { | |
| _client->abort(); | |
| } | |
| return; | |
| } | |
| memcpy(copy, data, datalen); | |
| copy[datalen] = 0; | |
| _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, copy, datalen); | |
| free(copy); | |
| // For non-text messages, however, we can avoid the extra allocation and pass the original data buffer directly. | |
| // ------------------------------------------------------------ | |
| uint8_t *copy = nullptr; | |
| void *eventData = (void *)data; | |
| // Only allocate a NUL-terminated copy for text messages to preserve legacy behavior. | |
| if (_pinfo.message_opcode == WS_TEXT) { | |
| copy = (uint8_t *)malloc(datalen + 1); | |
| if (copy == NULL) { | |
| async_ws_log_e("Failed to allocate"); | |
| _status = WS_DISCONNECTED; | |
| if (_client) { | |
| _client->abort(); | |
| } | |
| return; | |
| } | |
| memcpy(copy, data, datalen); | |
| copy[datalen] = 0; | |
| eventData = copy; | |
| } | |
| _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, eventData, datalen); | |
| if (copy != nullptr) { | |
| free(copy); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Si my comment above (#385 (comment)).
This would be an optimization, yes, but we won't be able to know for the next frames (WS_CONTINUATION) if we have to copy or not since we would have lost at that time the info about te previous frame type.
Copilot
AI
Feb 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The copy+NUL-termination logic (and the long explanatory comment) is duplicated in two branches. This makes future fixes easy to miss and increases maintenance cost. Consider extracting this into a small helper (e.g., a local lambda that returns a temporary buffer or invokes the handler) so the behavior stays consistent across fragmented/non-fragmented paths.
| uint8_t *copy = (uint8_t *)malloc(datalen + 1); | |
| if (copy == NULL) { | |
| async_ws_log_e("Failed to allocate"); | |
| _status = WS_DISCONNECTED; | |
| if (_client) { | |
| _client->abort(); | |
| } | |
| return; | |
| } | |
| memcpy(copy, data, datalen); | |
| copy[datalen] = 0; | |
| _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, copy, datalen); | |
| if (_pinfo.final) { | |
| _pinfo.num = 0; | |
| } else { | |
| _pinfo.num += 1; | |
| } | |
| free(copy); | |
| auto handleDataFramePayload = [this](const uint8_t *payload, size_t payloadLen) -> bool { | |
| uint8_t *copy = static_cast<uint8_t *>(malloc(payloadLen + 1)); | |
| if (copy == nullptr) { | |
| async_ws_log_e("Failed to allocate"); | |
| _status = WS_DISCONNECTED; | |
| if (_client) { | |
| _client->abort(); | |
| } | |
| return false; | |
| } | |
| memcpy(copy, payload, payloadLen); | |
| copy[payloadLen] = 0; | |
| _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, copy, payloadLen); | |
| free(copy); | |
| return true; | |
| }; | |
| if (!handleDataFramePayload(data, datalen)) { | |
| return; | |
| } | |
| if (_pinfo.final) { | |
| _pinfo.num = 0; | |
| } else { | |
| _pinfo.num += 1; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Si my comment above (#385 (comment)).
This would be an optimization, yes, but we won't be able to know for the next frames (WS_CONTINUATION) if we have to copy or not since we would have lost at that time the info about te previous frame type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I decided to always do the copy regardless of the frame type (WS_TEXT, WS_BINARY, WS_CONTINUATION) because only the first frame holds the info WS_TEXT, WS_BINARY. Others are continuation frame.
So this is more correct by the spec since when in next frames we do not have the information about the message type, but we could decide to optimize and only do the copy when we encounter a WS_TEXT frame, because a frame max length is 2^63 so this is not likely that we would encounter continuation frames on a MCU...
I am opened to both solutions.