Skip to content

v0.51.0

Latest

Choose a tag to compare

@yhirose yhirose released this 24 Jul 02:56

What's Changed

Security fixes

  • Reject CR/LF in the request target and fail the request cleanly (Fix #2501). write_request_line already stripped CR/LF from the target, but ClientImpl::write_request ignored its -1 return on rejection, so the client silently sent a request-line-less (headers-only) request and reported it as successful. Reachable via a decoded redirect Location under set_path_encode(false), and via the CONNECT target. The client now fails with Error::Write instead of putting a malformed request on the wire
  • Match chunked as the final transfer coding, order-independently (Fix #2500). is_chunked_transfer_encoding compared the whole Transfer-Encoding value against "chunked", so a valid value naming another coding first (e.g. "gzip, chunked", RFC 9112 §6.1) was read as unframed on both server and client. On a keep-alive server the unparsed body could then be read back as a smuggled request; on the client, the stream desynced the same way. The helper now matches the last coding token case-insensitively, and any message across multiple Transfer-Encoding lines that names chunked is treated as chunked (fail-safe, since a mis-parse only closes the connection, whereas the opposite error enables smuggling)
  • Skip fields with invalid names/values when writing response headers, preventing response splitting (#2505). write_headers passes res.headers straight to the wire; unlike set_header(), it never validated for CR/LF, so an application populating res.headers directly from untrusted/request-derived input could inject headers or split the response. It now applies the same is_field_name/is_field_value guard as set_header(), consolidated into a single fields::is_field_valid() used by every header-writing path (#2506)
  • Flag out-of-range Content-Length instead of silently truncating it. get_header_value_u64 cast the result of strtoull straight to size_t; a value that overflowed unsigned long long (saturates to ULLONG_MAX) or didn't fit size_t on a 32-bit build now sets the invalid-value flag instead of continuing with a bogus framing length
  • Strip Cookie/Cookie2 headers on cross-origin redirect, alongside the Host/Authorization/Proxy-Authorization headers already stripped, so session cookies aren't forwarded to a different origin
  • Validate the connecting peer before honoring X-Forwarded-For. Server::process_request only checked that trusted_proxies_ was non-empty, never that the actual TCP peer was itself a trusted proxy — so any client connecting directly could spoof remote_addr with an arbitrary X-Forwarded-For header. It's now only honored when the connecting peer matches an entry in trusted_proxies_
  • Scan X-Forwarded-For right-to-left in get_client_ip (#2503). Each hop appends the address it received the request from, so the rightmost entries are the ones written by trusted infrastructure. The previous left-to-right scan let a client forge an arbitrary client address by following it with a trusted proxy's address
  • Limit the number of header lines per multipart form-data part (#2497). Each line was already bounded by CPPHTTPLIB_HEADER_MAX_LENGTH, but a single part had no cap on how many small header lines it could contain, and each is parsed twice — packing many into one part inflated CPU usage within the overall payload limit. Now capped by CPPHTTPLIB_HEADER_MAX_COUNT, mirroring the limit read_headers() already enforces for request headers

New features

  • Add Mbed TLS 4.x (PSA Crypto) support, auto-detected via MBEDTLS_VERSION_MAJOR (#2502). Hashing (MD5/SHA-256/SHA-512) moves to psa_hash_compute, the explicit entropy/CTR-DRBG RNG is dropped in favor of PSA's, and TLS 1.3 session-ticket retries are handled uniformly across connect/read/write. macOS CI now covers 4.x via Homebrew; a new ubuntu-26.04 job covers 3.6, alongside the existing 2.28 coverage on ubuntu-latest. Documented in README and the tour's TLS setup pages, including the libmbedcryptolibtfpsacrypto rename

Bug fixes

  • Fix mbedTLS is_peer_closed() destroying the first byte of the response. Mbed TLS has no SSL_peek() equivalent, so the liveness probe run after every write performed a real 1-byte mbedtls_ssl_read() and discarded it — if the response had already arrived, this silently ate the first byte of the status line. This was the root cause of the long-standing MbedTLS-only CI flakiness; the probed byte is now pushed back into the session so no data is lost, and the previously reduced mbedTLS CI shard counts are restored to default
  • Skip the post-response body drain once the connection is already committed to closing (#2504). The drain exists to protect a subsequent request on a persistent connection from unread framed body bytes; once the response has committed to Connection: close, that protection is moot, and continuing to drain could block indefinitely on an aborted, unterminated chunked upload — delaying the transport close that would otherwise tell the uploader to stop

Full Changelog: v0.50.1...v0.51.0