Skip to content

Commit b5f5cf7

Browse files
Merge pull request #445 from ESP32Async/CWE-190
Fix CWE-190 - DoS could happen with a specially crafted boundary parameter in a multipart request
2 parents a87fcdb + b928de7 commit b5f5cf7

4 files changed

Lines changed: 144 additions & 12 deletions

File tree

src/AsyncWebHeader.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ const AsyncWebHeader AsyncWebHeader::parse(const char *data) {
2323
return AsyncWebHeader(); // Header name cannot be empty
2424
}
2525
const char *startOfValue = colon + 1; // Skip the colon
26-
// skip one optional whitespace after the colon
27-
if (*startOfValue == ' ') {
26+
// RFC 7230 §3.2.3: OWS (optional whitespace) = *( SP / HTAB )
27+
// Skip all leading spaces and tabs, not just the first one.
28+
while (*startOfValue == ' ' || *startOfValue == '\t') {
2829
startOfValue++;
2930
}
3031
String name;

src/ESPAsyncWebServer.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,9 @@ class AsyncWebServerRequest {
465465
std::unordered_map<const char *, String, std::hash<const char *>, std::equal_to<const char *>> _attributes;
466466

467467
uint8_t _multiParseState;
468-
uint8_t _boundaryPosition;
468+
// CWE-190 fix: was uint8_t, which would overflow to 0 for boundaries >= 256
469+
// bytes and cause the BOUNDARY_OR_DATA parser loop to never terminate (DoS).
470+
size_t _boundaryPosition;
469471
size_t _itemStartIndex;
470472
size_t _itemSize;
471473
String _itemName;

src/WebRequest.cpp

Lines changed: 136 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,132 @@ bool AsyncWebServerRequest::_parseReqHeader() {
473473
_host = value;
474474
} else if (name.equalsIgnoreCase(T_Content_Type)) {
475475
_contentType = value.substring(0, value.indexOf(';'));
476-
if (value.startsWith(T_MULTIPART_)) {
477-
_boundary = value.substring(value.indexOf('=') + 1);
478-
_boundary.replace(String('"'), String());
476+
// Trim _contentType defensively; AsyncWebHeader::parse now strips all
477+
// leading OWS per RFC 7230, but trim() guards against any future change.
478+
_contentType.trim();
479+
// Media types are case-insensitive (RFC 2045 §5.1), so lowercase the
480+
// entire header value once and reuse it for all subsequent searches.
481+
String lowcase(value);
482+
lowcase.toLowerCase();
483+
if (lowcase.startsWith(T_MULTIPART_)) {
484+
// Case-insensitive, quote-aware search for the boundary parameter.
485+
// We scan forward tracking quoted-string state so that a 'boundary='
486+
// substring inside a quoted parameter value (e.g. foo="x; boundary=y")
487+
// is never mistaken for the real parameter delimiter. A ';' inside a
488+
// quoted-string is not a parameter separator per RFC 2045 §5.1.
489+
// We also require 'boundary=' to be immediately preceded by ';' (with
490+
// optional OWS) so that e.g. 'x-boundary=' is not matched.
491+
int bpos = -1;
492+
bool inQuotes = false;
493+
for (int i = 0; i < (int)lowcase.length(); i++) {
494+
char c = lowcase.charAt(i);
495+
if (inQuotes) {
496+
if (c == '\\' && i + 1 < (int)lowcase.length()) {
497+
i++; // skip quoted-pair — the escaped character cannot terminate the quoted-string
498+
} else if (c == '"') {
499+
inQuotes = false;
500+
}
501+
} else {
502+
if (c == '"') {
503+
inQuotes = true;
504+
} else if (c == ';') {
505+
// Skip OWS after the ';' and check for 'boundary='
506+
int j = i + 1;
507+
while (j < (int)lowcase.length() && (lowcase.charAt(j) == ' ' || lowcase.charAt(j) == '\t')) {
508+
j++;
509+
}
510+
// Use strncmp on the raw C string to avoid a heap allocation
511+
// from String::substring() — this code runs on attacker-controlled
512+
// input in a scan loop, so zero-allocation is preferred.
513+
if ((size_t)j + T_BOUNDARY_LEN <= lowcase.length() && std::strncmp(lowcase.c_str() + j, T_BOUNDARY, T_BOUNDARY_LEN) == 0) {
514+
bpos = j;
515+
break;
516+
}
517+
}
518+
}
519+
}
520+
521+
if (bpos < 0) {
522+
// No valid boundary parameter found — cannot parse multipart body.
523+
async_ws_log_d("Missing multipart boundary parameter, aborting");
524+
_parseState = PARSE_REQ_FAIL;
525+
abort();
526+
return true;
527+
}
528+
529+
// Extract the boundary value that follows "boundary=" and strip leading/
530+
// trailing whitespace. The value may be either a token or a
531+
// quoted-string (RFC 2045 §5.1 / RFC 2046 §5.1).
532+
_boundary = value.substring(bpos + (int)T_BOUNDARY_LEN);
533+
_boundary.trim();
534+
535+
if (_boundary.startsWith("\"")) {
536+
// Quoted-string form: scan forward from position 1 for the closing
537+
// double-quote. A quote is escaped only when preceded by an ODD
538+
// number of consecutive backslashes (e.g. \" is escaped, \\" is not
539+
// because the two backslashes escape each other, leaving the quote
540+
// unescaped). Checking only the immediately preceding character
541+
// would mishandle the \\" case.
542+
int endQuote = 1;
543+
while (true) {
544+
endQuote = _boundary.indexOf('"', endQuote);
545+
if (endQuote < 0) {
546+
break; // string ran out — unterminated quote
547+
}
548+
// Count consecutive backslashes immediately before this quote.
549+
int backslashes = 0;
550+
while (endQuote - 1 - backslashes >= 0 && _boundary.charAt(endQuote - 1 - backslashes) == '\\') {
551+
backslashes++;
552+
}
553+
if (backslashes % 2 == 0) {
554+
break; // even number of backslashes → quote is real closing quote
555+
}
556+
endQuote++; // odd number → quote is escaped, keep scanning
557+
}
558+
if (endQuote < 0) {
559+
// Opening quote was never closed — malformed header.
560+
async_ws_log_d("Invalid multipart boundary (unterminated quote), aborting");
561+
_parseState = PARSE_REQ_FAIL;
562+
abort();
563+
return true;
564+
}
565+
// Strip the surrounding quotes; content between them is the boundary.
566+
_boundary = _boundary.substring(1, endQuote);
567+
568+
// Unescape quoted-pair sequences so the boundary matches the actual bytes on the wire.
569+
String unescaped;
570+
unescaped.reserve(_boundary.length());
571+
for (size_t i = 0; i < _boundary.length(); ++i) {
572+
char c = _boundary.charAt(i);
573+
if (c == '\\' && (i + 1) < _boundary.length()) {
574+
c = _boundary.charAt(++i);
575+
}
576+
unescaped += c;
577+
}
578+
_boundary = unescaped;
579+
} else {
580+
// Token form: the value ends at the next ';' (start of the next
581+
// parameter) or at the end of the header field.
582+
int semi = _boundary.indexOf(';');
583+
if (semi >= 0) {
584+
_boundary = _boundary.substring(0, semi);
585+
}
586+
_boundary.trim();
587+
}
588+
589+
// CWE-190 / DoS fix: RFC 2046 §5.1 limits boundary strings to 70
590+
// characters. _boundaryPosition was formerly uint8_t, so a boundary
591+
// of exactly 256 bytes would overflow the counter to 0, preventing the
592+
// BOUNDARY_OR_DATA loop from ever reaching the end of the boundary and
593+
// causing unbounded CPU consumption (watchdog reset on ESP32/ESP8266).
594+
// Reject boundaries outside the valid range before any parsing begins.
595+
if (_boundary.length() == 0 || _boundary.length() > 70) {
596+
async_ws_log_d("Invalid multipart boundary length (%u), aborting", _boundary.length());
597+
_parseState = PARSE_REQ_FAIL;
598+
abort();
599+
return true;
600+
}
601+
479602
_isMultipart = true;
480603
}
481604
} else if (name.equalsIgnoreCase(T_Content_Length) || name.equalsIgnoreCase(T_X_Expected_Entity_Length)) {
@@ -628,6 +751,11 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
628751
_multiParseState = EXPECT_FEED1;
629752
}
630753
} else if (_multiParseState == EXPECT_BOUNDARY) {
754+
// Note: when _parsedLength < 2, the subtractions below (_parsedLength - 2
755+
// and _parsedLength - 3) wrap around to very large size_t values. This is
756+
// intentional: the wrapped values are always greater than _boundary.length()
757+
// (which is at most 70), so those comparisons evaluate to false and the
758+
// first two bytes (the '--' prefix) are consumed without error.
631759
if (_parsedLength < 2 && data != '-') {
632760
_multiParseState = PARSE_ERROR;
633761
return;
@@ -743,8 +871,9 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
743871
itemWriteByte('\n');
744872
itemWriteByte('-');
745873
itemWriteByte('-');
746-
uint8_t i;
747-
for (i = 0; i < _boundaryPosition; i++) {
874+
// CWE-190 fix: loop variable was uint8_t, matching the old type of
875+
// _boundaryPosition. Changed to size_t for consistency.
876+
for (size_t i = 0; i < _boundaryPosition; i++) {
748877
itemWriteByte(_boundary.c_str()[i]);
749878
}
750879
_parseMultipartPostByte(data, last);
@@ -784,8 +913,7 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
784913
itemWriteByte('\n');
785914
itemWriteByte('-');
786915
itemWriteByte('-');
787-
uint8_t i;
788-
for (i = 0; i < _boundary.length(); i++) {
916+
for (size_t i = 0; i < _boundary.length(); i++) {
789917
itemWriteByte(_boundary.c_str()[i]);
790918
}
791919
_parseMultipartPostByte(data, last);
@@ -800,8 +928,7 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
800928
itemWriteByte('\n');
801929
itemWriteByte('-');
802930
itemWriteByte('-');
803-
uint8_t i;
804-
for (i = 0; i < _boundary.length(); i++) {
931+
for (size_t i = 0; i < _boundary.length(); i++) {
805932
itemWriteByte(_boundary.c_str()[i]);
806933
}
807934
itemWriteByte('\r');

src/literals.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ static constexpr const char T_username[] = "username";
108108
static constexpr const char T_WS[] = "websocket";
109109
static constexpr const char T_WWW_AUTH[] = "WWW-Authenticate";
110110
static constexpr const char T_X_Expected_Entity_Length[] = "X-Expected-Entity-Length";
111+
static constexpr const char T_BOUNDARY[] = "boundary=";
112+
static constexpr size_t T_BOUNDARY_LEN = sizeof(T_BOUNDARY) - 1;
111113

112114
// HTTP Methods
113115
static constexpr const char T_GET[] = "GET";

0 commit comments

Comments
 (0)