What's Changed
Security fixes
- Reject CR/LF in the request target and fail the request cleanly (Fix #2501).
write_request_linealready stripped CR/LF from the target, butClientImpl::write_requestignored its-1return on rejection, so the client silently sent a request-line-less (headers-only) request and reported it as successful. Reachable via a decoded redirectLocationunderset_path_encode(false), and via theCONNECTtarget. The client now fails withError::Writeinstead of putting a malformed request on the wire - Match
chunkedas the final transfer coding, order-independently (Fix #2500).is_chunked_transfer_encodingcompared the wholeTransfer-Encodingvalue 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 multipleTransfer-Encodinglines that nameschunkedis 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_headerspassesres.headersstraight to the wire; unlikeset_header(), it never validated for CR/LF, so an application populatingres.headersdirectly from untrusted/request-derived input could inject headers or split the response. It now applies the sameis_field_name/is_field_valueguard asset_header(), consolidated into a singlefields::is_field_valid()used by every header-writing path (#2506) - Flag out-of-range
Content-Lengthinstead of silently truncating it.get_header_value_u64cast the result ofstrtoullstraight tosize_t; a value that overflowedunsigned long long(saturates toULLONG_MAX) or didn't fitsize_ton a 32-bit build now sets the invalid-value flag instead of continuing with a bogus framing length - Strip
Cookie/Cookie2headers on cross-origin redirect, alongside theHost/Authorization/Proxy-Authorizationheaders already stripped, so session cookies aren't forwarded to a different origin - Validate the connecting peer before honoring
X-Forwarded-For.Server::process_requestonly checked thattrusted_proxies_was non-empty, never that the actual TCP peer was itself a trusted proxy — so any client connecting directly could spoofremote_addrwith an arbitraryX-Forwarded-Forheader. It's now only honored when the connecting peer matches an entry intrusted_proxies_ - Scan
X-Forwarded-Forright-to-left inget_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 byCPPHTTPLIB_HEADER_MAX_COUNT, mirroring the limitread_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 topsa_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 newubuntu-26.04job covers 3.6, alongside the existing 2.28 coverage onubuntu-latest. Documented in README and the tour's TLS setup pages, including thelibmbedcrypto→libtfpsacryptorename
Bug fixes
- Fix mbedTLS
is_peer_closed()destroying the first byte of the response. Mbed TLS has noSSL_peek()equivalent, so the liveness probe run after every write performed a real 1-bytembedtls_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