@@ -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 ' );
0 commit comments