99#include < algorithm>
1010#include < cstring>
1111#include < memory>
12+ #include < string_view>
1213#include < utility>
1314
1415#include " ./literals.h"
@@ -490,10 +491,17 @@ bool AsyncWebServerRequest::_parseReqHeader() {
490491 // optional OWS) so that e.g. 'x-boundary=' is not matched.
491492 int bpos = -1 ;
492493 bool inQuotes = false ;
493- for (int i = 0 ; i < (int )lowcase.length (); i++) {
494- char c = lowcase.charAt (i);
494+ const char * lc = lowcase.c_str ();
495+ const int llen = (int )lowcase.length ();
496+ // Stop early: a ';' followed by OWS and 'boundary=' needs at least
497+ // T_BOUNDARY_LEN+1 chars after it. Without OWS the minimum is
498+ // i+1+T_BOUNDARY_LEN+1 <= llen, i.e. i < llen-(T_BOUNDARY_LEN+1).
499+ // The inner OWS-aware check below still guards the OWS case.
500+ const int lscan = llen - (int )(T_BOUNDARY_LEN + 1 );
501+ for (int i = 0 ; i < lscan; i++) {
502+ char c = lc[i];
495503 if (inQuotes) {
496- if (c == ' \\ ' && i + 1 < ( int )lowcase. length () ) {
504+ if (c == ' \\ ' && i + 1 < llen ) {
497505 i++; // skip quoted-pair — the escaped character cannot terminate the quoted-string
498506 } else if (c == ' "' ) {
499507 inQuotes = false ;
@@ -504,13 +512,17 @@ bool AsyncWebServerRequest::_parseReqHeader() {
504512 } else if (c == ' ;' ) {
505513 // Skip OWS after the ';' and check for 'boundary='
506514 int j = i + 1 ;
507- while (j < ( int )lowcase. length () && (lowcase. charAt (j) == ' ' || lowcase. charAt (j) == ' \t ' )) {
515+ while (j < llen && (lc[j] == ' ' || lc[j] == ' \t ' )) {
508516 j++;
509517 }
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 ) {
518+ // If there is not enough room left for "boundary=" plus at least
519+ // one value byte, no later ';' can match either — stop scanning.
520+ if (j + (int )T_BOUNDARY_LEN + 1 > llen) {
521+ break ;
522+ }
523+ // strncmp stops at the null terminator, so no separate length
524+ // guard is needed: a short suffix causes a non-matching result.
525+ if (std::strncmp (lc + j, T_BOUNDARY , T_BOUNDARY_LEN ) == 0 ) {
514526 bpos = j;
515527 break ;
516528 }
@@ -526,64 +538,62 @@ bool AsyncWebServerRequest::_parseReqHeader() {
526538 return true ;
527539 }
528540
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++;
541+ // Use a std::string_view into the original (mixed-case) header value
542+ // to locate and extract the boundary without any intermediate heap
543+ // allocation. The value may be either a token or a quoted-string
544+ // (RFC 2045 §5.1 / RFC 2046 §5.1).
545+ std::string_view bv (value.c_str () + bpos + T_BOUNDARY_LEN ,
546+ value.length () - bpos - T_BOUNDARY_LEN );
547+
548+ if (!bv.empty () && bv[0 ] == ' "' ) {
549+ // Quoted-string form: scan once from the opening '"', unescaping
550+ // quoted-pairs on the fly and writing into _boundary directly.
551+ // This single pass replaces the previous three-pass approach
552+ // (find close-quote, substring, then separate unescape loop).
553+ bv.remove_prefix (1 ); // skip opening '"'
554+ _boundary.reserve (bv.size ());
555+ bool closed = false ;
556+ for (size_t i = 0 ; i < bv.size (); ++i) {
557+ char c = bv[i];
558+ if (c == ' \\ ' && i + 1 < bv.size ()) {
559+ _boundary += bv[++i]; // quoted-pair: keep only the escaped char
560+ } else if (c == ' "' ) {
561+ closed = true ;
562+ break ;
563+ } else {
564+ _boundary += c;
552565 }
553- if (backslashes % 2 == 0 ) {
554- break ; // even number of backslashes → quote is real closing quote
566+ if (_boundary.length () > 70 ) {
567+ async_ws_log_d (" Invalid multipart boundary length (%u), aborting" , _boundary.length ());
568+ _parseState = PARSE_REQ_FAIL ;
569+ abort ();
570+ return true ;
555571 }
556- endQuote++; // odd number → quote is escaped, keep scanning
557572 }
558- if (endQuote < 0 ) {
573+ if (!closed ) {
559574 // Opening quote was never closed — malformed header.
560575 async_ws_log_d (" Invalid multipart boundary (unterminated quote), aborting" );
561576 _parseState = PARSE_REQ_FAIL ;
562577 abort ();
563578 return true ;
564579 }
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;
579580 } 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);
581+ // Token form: value ends at the next ';' or end of string.
582+ // Trim trailing OWS using the view — no copy until the final assign.
583+ auto semi = bv.find (' ;' );
584+ if (semi != std::string_view::npos) {
585+ bv = bv.substr (0 , semi);
586+ }
587+ while (!bv.empty () && (bv.back () == ' ' || bv.back () == ' \t ' )) {
588+ bv.remove_suffix (1 );
589+ }
590+ if (bv.empty () || bv.size () > 70 ) {
591+ async_ws_log_d (" Invalid multipart boundary length (%u), aborting" , (unsigned )bv.size ());
592+ _parseState = PARSE_REQ_FAIL ;
593+ abort ();
594+ return true ;
585595 }
586- _boundary. trim ( );
596+ _boundary = String (bv. data (), ( unsigned int )bv. size () );
587597 }
588598
589599 // CWE-190 / DoS fix: RFC 2046 §5.1 limits boundary strings to 70
0 commit comments