What's Changed
Security-relevant fixes
- Reject control characters in the chunk-size line (#2585).
ChunkedDecoder::read_payload()checked only the first byte after the chunk-size, andgetline()reads up to the CRLF, so a bare LF (or CR, or any other control byte) after the size or inside a chunk-ext was swallowed as extension text, while an intermediary that ends the line on that LF delimits the chunks differently, and the two disagree on where the body ends (request smuggling, RFC 9112 §7.1.1). The rest of the line is now validated aschunk-extwhere it is parsed, which covers the server and both client read paths since they share this decoder. Ordinary extensions are still accepted. Every scan of the line is also bounded by the line terminator instead of by the buffer's NUL, so the terminator can never be read as line content. - Bound the trailer declaration set and count every received trailer field (#2583).
parse_trailers()combined allTrailerfield values and inserted each declared name into a local case-insensitive set with no cap, so a peer could grow that set well past the 100-field header limit (case_ignore::hashis unkeyed, so colliding names turn lookups quadratic). The set is now capped atCPPHTTPLIB_HEADER_MAX_COUNT. Separately, the received-trailer counter advanced only for declared fields that were stored, so undeclared fields could keep the loop running past the limit; every received field now counts.
Bug fixes
- Run
pre_request_handlerfor WebSocket routes. The upgrade path matched the route and switched protocols without settingreq.matched_routeor callingpre_request_handler, so a check placed there (authentication, for instance) never ran for WebSocket routes.matched_routeis now set and the handler runs before the upgrade; if it handles the request, the server replies with a regular HTTP response instead of 101. Rejected upgrade responses (frompre_routing_handlertoo) are now written withContent-Length, so a client reading the body no longer waits for the keep-alive timeout. - Send
100 Continueonly when the request body is about to be read. The server wrote the interim response as soon as it saw the expectation, beforepre_routing_handler,pre_request_handleror routing ran, so a request that those handlers rejected, or one that matched no route, still invited the client to send a body the server would never read. If the request is answered without reading the body,100 Continueis never sent and the connection is closed, since whether and when the client sends the body is unknown. A 417 returned byexpect_100_continue_handleris now the final response; it used to be written as a bare status line, after which the request was processed and a second response was written. - Don't wait for the peer's
close_notifyon OpenSSL shutdown.tls::shutdown()calledSSL_shutdown()a second time to wait for it, but an idle keep-alive client never sends one, so closing its connection held the worker thread until the read timeout andServer::stop()waited for it. The backend now sendsclose_notifyand returns, as the Mbed TLS and wolfSSL backends already do.
Documentation
- README and cookbook S18 (en/ja): document the default port-sharing behavior (
SO_REUSEPORTon Linux/macOS,SO_REUSEADDRon Windows) and how to bind exclusively withset_socket_options(), including whySO_REUSEADDRalone is not enough on Windows.bind_to_port()returningfalseis no longer described as "port already in use", which it is not by default. - README, README-websocket and the tour/cookbook pages (en/ja): document that
pre_request_handlerruns for WebSocket routes.
Development
scripts/release.sh: update the version files withsed -i.bak, which GNU sed accepts too.sed -i ''is BSD-only, sojust release --runfailed on Linux before touching anything.
Full Changelog: v0.56.0...v0.57.0