A retry or redirect after an early answer to Expect: 100-continue now
opens a new connection, interim responses are also skipped while
waiting for 100 Continue, and a URL Client cannot use, such as one with
userinfo, gives an invalid client. Only a 407 is passed on from a
refused CONNECT, WebSocket::close() sends only a valid payload, and the
SSE client stops on an oversized event rather than reconnect to it.
Media type parameter names and user extension mappings are matched
without case, file content is read in larger chunks from a buffer of
its own, the streaming decompression limit reports
ExceedMaxPayloadSize, and the tests use the existing single-response
server helpers. Comments and the README are brought in line.
The server fuzzer's stream exposed no buffered data, so the fast path of
stream_line_reader::getline was never fuzzed. Hand out what a read
pulled in beyond the bytes asked for, as SocketStream does.
Over TLS a file is copied out of its mapping in this process, so one
truncated while it was being served raised SIGBUS and took the whole
server down. Read it with pread, which comes up short instead.
Credentials and the client certificate stay behind; other headers,
default ones included, go along as RFC 9110 15.4 has it, and so do the
limits and the TLS settings.
A wss client could set its CAs but had no way to pin a certificate. Add
set_server_certificate_verifier() and set_session_verifier(), as
SSLClient has.
INDEX.HTML got the default type, a REMOTE_ADDR spelled in lower case got
past the guard on prohibited header names, and Multipart/Form-Data was
not taken for multipart, though RFC 9110 makes field names and media
types case-insensitive and web servers match extensions so.
OpenSSL allowed client-initiated renegotiation, which runs a handshake
update_certs_pem() does not wait for, and kept sessions and ticket keys
across a change of client CAs, so a client verified under the old ones
could resume. wolfSSL added new client CAs to the old instead of
replacing them, and a bad certificate or key left Mbed TLS and wolfSSL
half updated.
Turn renegotiation off, forget the sessions and ticket keys on a client
CA change, replace wolfSSL's CAs, and check new material before swapping
it in. Note that the update must not be called from a TLS callback.
The Mbed TLS and wolfSSL backends matched *.com, a bare *, vic*.test and
names with empty labels, which OpenSSL rejects. Require two labels after
a wildcard, none in an IDNA label and no empty label, and ignore a
trailing dot on the host, so every backend accepts the same names.
On Mbed TLS and wolfSSL the verification mode is a setting of the
context, and it was changed outside the lock that guards creating a
session from that context.
Any Close payload was echoed back, a 1-byte one, a code that may not be
sent and a reason that is not UTF-8 included. Echo a valid one and
answer the others with 1002, from one place for both read loops.
A non-200 answer to CONNECT was returned as the response, so a proxy, or
anyone on the plain leg to it, could answer an HTTPS request as if the
origin had. Fail the request with Error::ProxyConnection for anything
but a 4xx or 5xx refusal.
Only 100 Continue was skipped, and only without header fields, so a 103
Early Hints was taken for the final response and a 100 with fields threw
the parsing off. Skip every 1xx but 101, along with its header fields.
When the final response came before 100 Continue, the body was never
sent but the connection was kept, so the server could read the next
request as that body. Close it after the response.
parse_url took http://user@evil.com/ for the host "user@evil.com", where
parsers that honour userinfo, or read a backslash as '/', see evil.com.
userinfo is not supported, so reject both.
When the real path of a file under a mount point could not be resolved,
the check that it stays inside the mount point was skipped and the file
was served. Do not serve it from there, and refuse a mount point whose
directory cannot be resolved.
decode_path_component dropped a %u escape of a surrogate, joining what
was on either side of it, so /ad%uD800min became /admin after a filter
had seen something else. Keep such an escape as it is.
Any run of spaces or tabs separated the parts of the request line, a
fragment in the target was stripped, and an HTTP/1.1 request without
Host was served, so a server or a cache in front that follows RFC 9112
could see another request. Require a single SP between the parts, and
answer a target with '#' and an HTTP/1.1 request without Host with 400.
The client no longer sends the fragment of a URL.
RFC 9112 6.1 has a server close the connection after answering a
request with both Transfer-Encoding and Content-Length, or with
Transfer-Encoding in HTTP/1.0. Close it, and send Connection: close
whenever the connection closes after a response.
split_unquoted ended a quoted-string at an escaped quote, so
name="a\"; filename=evil.php" came apart into a name and a filename that
a parser in front following RFC 9110 would not see. Skip quoted-pairs
when splitting, and unescape \" and \\ in Content-Disposition values,
leaving other backslashes, such as those of a Windows path, as they are.
A part header arriving a few bytes at a time was searched for its CRLF
from the start on every read, which made parsing it quadratic. Remember
how far the search got.
The SSE client buffered a line, or the data of an event, with no limit
of its own, and with payload_max_length(0) with none at all. Add
CPPHTTPLIB_SSE_MAX_EVENT_LENGTH (16MB) and end the connection with
ExceedMaxPayloadSize past it.
Every continuation frame could be as long as the whole message limit,
and the total was checked only after appending it, so a fragmented
message could take about twice the limit. Read each fragment against
what is left, with room for a control frame between them.
The client reserved what Content-Length claimed, up to
payload_max_length, before any of the body arrived, and without a limit
nothing bounded it. Reserve at most 1 MiB up front and let a longer body
grow as it is read.
An unsupported scheme without exceptions, or a port out of range, left
Client with no implementation, so the first request after it went
through a null pointer. Put a client in its place that reports itself
invalid and fails every request with Error::Connection.
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH = 0 rejected every frame with a
payload, while 0 means no limit for payload_max_length. Make it mean the
same here, and refuse a length that size_t cannot hold.
set_payload_max_length(0) means no limit, but StreamHandle compared the
decompressed size against 0, so every compressed response read through
the streaming API failed.
case_ignore::hash recursed once per character of the key, and trailer
field names from the wire reach it, so a long one could overflow the
stack of a build that keeps the recursion (-O0). Hash in a loop instead,
which gives the same values.
Remove the now-unused forward declaration and the null checks that can no
longer fire, since create_session() always sets the session and its
context before installing the callback, and shorten the comments.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8
find_content_type(), can_compress_content_type() and
is_prohibited_header_name() switched on a 26-bit hash of the name and
never compared the name itself, so any name that hashed alike was taken
for a known one. An uploaded file with an unknown extension could be
served as text/html, for one.
Compare the names themselves, and drop the hash and its literal
operator, which nothing else uses.
Every session now gets mbedtls_verify_callback(), which reads the user's
callback from the owning context when the handshake runs. The
has_verify_callback flag and the separate hostname-masking callback are no
longer needed, and a callback set after a session was created is still
honoured.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8
* Carry the server certificate verifier over to the redirect client
create_redirect_client() now copies the CA store and the session verifier
to the SSLClient it builds for a cross-host https redirect (#2632), but a
verifier given to set_server_certificate_verifier() was still dropped: the
redirect client's ctx never had the verify callback wired in, so a
pinning verifier never ran on the host the server redirects to.
SSLClient now keeps its own copy of the verifier, since ctx_ cannot report
it back and the backends hold it in a slot shared by every client, and
installs it on the redirect client. The copy also replaces the
Windows-only server_certificate_verifier_set_ flag.
SSLClientRedirectTest.ServerCertificateVerifier fails before this change
and passes after (OpenSSL 3, Mbed TLS 2.28, wolfSSL 5.6).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8
* Clarify the server_certificate_verifier_ comment
The backends keep the verify callback in a per-thread slot, not one
shared by every client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8
---------
Co-authored-by: Claude <noreply@anthropic.com>
tls::set_verify_callback() kept the callback in a thread_local slot, so two
SSLClients configured on one thread overwrote each other's verifier, and a
client used from another thread ran none. Keep it with the context instead
(OpenSSL SSL_CTX ex_data, MbedTlsContext/WolfSSLContext member) and have each
backend trampoline look it up from the session's context.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wxevjmRqzpy8qDAD3vdwr
update_certs_pem() replaced the certificate, key and client CAs inside
the TLS context while handshakes on other threads were still reading
them, and only the creation of a session was serialized with it. A
server rotating its certificates under load could crash.
Count the handshakes in progress, make an update wait until there are
none, and hold off new handshakes while an update is pending so that a
steady stream of them cannot keep it waiting. The wait is bounded by
the handshake timeout.
The handshake loops gave every wait for the socket the full timeout
again, so a peer that sent a byte now and then kept a handshake going,
and the worker serving it, for as long as it liked. Work out the
deadline once and wait only for what is left of it, on both sides and in
every backend.
A cross-host redirect is followed by a new client, which did not get the
payload limit, the max timeout, the connection timeout's microseconds or
the hostname address map. Over HTTPS it also lost the session verifier,
the server certificate verifier and a store given to set_ca_cert_store(),
and fell back to the default CAs. Copy them over. The store belongs to
the original TLS context, so pass on the CA certificates it holds as PEM.
After the redirect, the original client also answered a 401 or 407 from
the new host with its own digest credentials and sent that to its own
host, with the new host's path. Return the redirected response as it is,
since the client that followed it has already answered what it could.
read_websocket_frame() resized the payload to the length a frame header
declared before reading any of it, so a 14-byte header made the peer
allocate and zero-fill up to CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH and
hold it until the read timed out. 70 such headers took a server to over
1 GB, and a client reading from a hostile server was exposed the same way.
Read the payload in CPPHTTPLIB_RECV_BUFSIZ chunks and grow it by each one,
so memory follows the bytes that actually came in.
std::regex_match recursed once per character of a status line's reason
phrase, so a server that sent one as long as a whole line overflowed the
client's stack. file_extension() and append_query_params() ran the same
kind of regex over a request path and a caller's path.
Parse all three by hand, with the same results, except that a path that
starts with '?' now gets its parameters joined with '&'. Serving a static
file no longer defers the content type lookup to keep a regex off the 304
path, so it looks the type up once.
FileContentProviderStaysWithinFile, added in e374966, calls
detail::set_file_content_provider, which split.py moves into httplib.cc,
so test_split did not compile. Declare it in the test, as the other tests
that call into detail:: already do.
When a content receiver returned false, or threw, partway through a body,
the drain after the response read a whole Content-Length again from where
it stopped. It swallowed the start of the next request, so the rest was
parsed as a request of its own once the handler answered below 400. A
handler that called the content reader a second time read into the next
request the same way.
Mark the body once reading starts, and if it does not finish, answer with
Connection: close and never drain it from the middle. Let each request's
body be read only once.
The wolfSSL verify callback set VerifyContext::session to the raw WOLFSSL
pointer, while every other tls:: function takes the WolfSSLSession that
wraps it, so passing ctx.session on from a verifier read the wrong type.
Point each WOLFSSL back to its WolfSSLSession and hand that out instead,
as the OpenSSL and Mbed TLS backends already do with their own handles.
tls::set_verify_callback(), which set_server_certificate_verifier() uses,
stored the callback in a single thread_local slot shared by every context.
A verifier was skipped when the handshake ran on another thread, ran for
other clients on the thread that set it, and could run after its client
was destroyed.
Store it with the context instead: in SSL_CTX ex_data for OpenSSL, and in
MbedTlsContext and WolfSSLContext for Mbed TLS and wolfSSL.
set_content() did not drop a content provider installed earlier, and
apply_ranges() then took the body's size as the Content-Length while the
provider was still the one written. The provider was asked for more bytes
than it had: a file under a mount point went out under the error status a
file request handler set to hide it, its mapping was read past the end,
and a user provider read past its own data.
Make every content setter clear the other sources so the last one wins,
let a body assigned directly after a provider or a file win as well, and
never read past the mapping in the file content provider.