Commit Graph

1985 Commits

Author SHA1 Message Date
yhirose
cda2bb5342 Consolidate header field-pair validation into fields::is_field_valid
The is_field_name(name) && is_field_value(value) predicate was repeated
across five output paths (set_header, write_headers, write_content_chunked
trailer, perform_websocket_handshake, check_and_write_headers). Introduce
fields::is_field_valid(name, value) and route all five through it so the
CR/LF-injection guard has a single definition. No behavior change.
2026-07-23 17:34:57 -04:00
yhirose
613a41b49d Merge pull request #2505 from metsw24-max/write-headers-crlf-guard
skip invalid fields in write_headers to prevent response splitting
2026-07-23 17:32:22 -04:00
Sayed Kaif
695961f8ae skip invalid fields in write_headers to prevent response splitting 2026-07-23 21:42:37 +05:30
yhirose
c64bf21a5e Merge pull request #2504 from emreay-/skip-drain-on-closing-connection
Skip request body drain when connection will close
2026-07-22 12:01:20 -04:00
Emre Ay
8bbfc90380 Apply clang-format to test.cc and httplib.h
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 10:53:51 +03:00
yhirose
bf8baf0739 clang-format 2026-07-21 19:47:15 -04:00
yhirose
fd0c18b1b5 Validate connecting peer before honoring X-Forwarded-For
Server::process_request only checked that trusted_proxies_ was
non-empty before deriving req.remote_addr from the X-Forwarded-For
header. It never verified that the actual TCP peer (remote_addr) was
itself one of the trusted proxies, so any client connecting directly
to the server could spoof remote_addr simply by sending an arbitrary
X-Forwarded-For header.

Now X-Forwarded-For is only honored when the connecting peer address
matches an entry in trusted_proxies_.
2026-07-21 19:44:39 -04:00
yhirose
bbd56a7e2c Merge pull request #2497 from superm1/superm1/SWSPLAT-23636
Limit the number of header lines per multipart form-data part
2026-07-21 19:15:50 -04:00
yhirose
54b3c1d072 Add Mbed TLS 4.x support (PSA Crypto) (#2502)
* Add Mbed TLS 4.x support (PSA Crypto) for macOS

Auto-detect Mbed TLS 4.x via MBEDTLS_VERSION_MAJOR and adapt the backend:
- Include psa/crypto.h and drop the headers removed in 4.x (ctr_drbg,
  entropy, md5, sha*), gated behind the version macro.
- Compute MD5/SHA-256/SHA-512 via PSA (psa_hash_compute) and initialize
  PSA Crypto once with std::call_once.
- Drop the explicit entropy/CTR-DRBG RNG (PSA provides the TLS RNG) and
  skip the RNG-callback overloads of pk_parse_key/pk_check_pair on 4.x.
- Retry on a TLS 1.3 NewSessionTicket (the 4.x default) in connect, read,
  write and is_peer_closed via a single mbedtls_is_session_ticket() helper,
  so online HTTPS works, including large redirected downloads where the
  ticket arrives mid-write.

Note V4 implies V3, so 3.x-only paths now check V3 && !V4.

Build systems (macOS): the CMake config and pkg-config shipped by Homebrew
resolve 4.x transitively, so CMakeLists.txt and meson.build need no change
for linking; the Makefile links libtfpsacrypto when present, else
libmbedcrypto.

Tests: generate the encrypted client key as both PBES2-AES (3.6+/4.x,
OpenSSL, wolfSSL) and PBES1-3DES (Mbed TLS 2.28) and pick by version, since
4.x dropped DES and 2.28 lacks PBES2. Also generate the IP-host certs in
test/meson.build to match gen-certs.sh and CMakeLists.txt.

* CI: test Mbed TLS 4.x on macOS, 3.x on Ubuntu 26.04

Homebrew's default mbedtls is now 4.x, so switch the macOS build and CI job
to it (drop the mbedtls@3 pin). That leaves 3.x (Ubuntu 24.04 apt ships 2.28,
macOS now 4.x) uncovered, so add an ubuntu-26.04 job whose apt provides Mbed
TLS 3.6. Net coverage: 2.28 (ubuntu-latest), 3.6 (ubuntu-26.04), 4.2 (macOS).

ubuntu-26.04 is a public-preview runner image; fold it into the main ubuntu
matrix once ubuntu-latest moves to 26.04.

* Document Mbed TLS 4.x support and libtfpsacrypto rename

Update README.md and the tour's TLS setup pages (en/ja) to note that
Mbed TLS 4.x is now auto-detected and that it renames libmbedcrypto
to libtfpsacrypto.
2026-07-21 18:45:48 -04:00
metsw24-max
6c4cbd49a2 scan X-Forwarded-For right-to-left in get_client_ip (#2503) 2026-07-21 18:12:40 -04:00
Emre Ay
f3e9a4d887 Skip request body drain when connection will close
The post-response drain exists to keep unread framed body bytes from being parsed as a subsequent request on a persistent connection. Once response generation has committed the connection to close, there can be no subsequent request, so draining no longer provides that protection.

Continuing to drain is especially harmful when a ContentReader aborts an unterminated chunked upload: the server can wait indefinitely for the terminal chunk even after sending Connection: close. This delays the transport close that tells an in-flight uploader to stop and leaves a worker occupied consuming discarded data.

Use the finalized response Connection header as the single source of truth for whether to skip the drain. write_response_core already sets this header for keep-alive exhaustion, request-directed closure, handler-directed closure, and error responses. Marking connection_closed then terminates the keep-alive loop and closes the socket.

Add a raw-socket regression test whose ContentReader rejects the first chunk of an unterminated upload. The test verifies that the 409 response announces Connection: close and that the peer observes EOF rather than timing out while the server drains.
2026-07-20 20:36:06 +03:00
Mario Limonciello
877a52f6b3 Limit the number of header lines per multipart form-data part
The multipart/form-data parser bounded the length of each individual
header line via CPPHTTPLIB_HEADER_MAX_LENGTH but did not cap how many
header lines a single part could contain. Within the overall payload
limit, a client could pack a large number of small header lines into
one part, each of which is parsed twice, inflating CPU usage.

Add a per-part header-line counter that resets when a new entry begins
and abort parsing once it reaches CPPHTTPLIB_HEADER_MAX_COUNT, mirroring
the limit already enforced by read_headers() for request headers.
2026-07-18 19:20:51 -05:00
yhirose
ef2beaea34 Fix MSVC C4146 warning for unsigned types in from_chars
The from_chars template is instantiated with unsigned types (e.g.
uint64_t for Content-Length), where unary minus on `result` triggers
MSVC warning C4146, failing the 32-bit build under warnings-as-errors.
Use `T(0) - result`, which yields the identical two's-complement value
without the warning.
2026-07-18 20:08:33 -04:00
yhirose
3adc525cec Use detail::from_chars for out-of-range Content-Length check
Replace the strtoull + errno + cast-back dance in get_header_value_u64
with the existing hand-written detail::from_chars, which reports
result_out_of_range at size_t width. This detects the 32-bit truncation
case directly (instead of via a separate cast-back comparison), drops
the reliance on the global errno, and keeps the parsing locale-
independent and consistent with the rest of the codebase.
2026-07-18 18:29:17 -04:00
Sayed Kaif
a415362dff update OpenStreamMalformedContentLength.OutOfRange for stricter parsing 2026-07-18 18:29:16 -04:00
Sayed Kaif
982235c0a9 flag out-of-range Content-Length in get_header_value_u64 2026-07-18 18:29:16 -04:00
yhirose
255c075b82 Reject CR/LF in the request target and fail the request cleanly (#2501)
* reject crlf in request target in write_request_line

* declare write_request_line in test.cc for split builds

split.py strips `inline` and moves the definition into httplib.cc, so
detail::write_request_line is not visible from the split httplib.h and
test_split failed to compile. Re-declare it in test.cc, matching what the
base64_encode and getaddrinfo_with_timeout tests already do.

* Fail the request when write_request_line rejects the target

The CR/LF guard in write_request_line returns -1, but ClientImpl::write_request
ignored that return value. A rejected target therefore produced a request-line-
less request (headers only) that the client silently reported as a successful
send. This is the primary path reachable via a decoded redirect Location under
set_path_encode(false), and it also carries the CONNECT target.

Check the return value like ClientImpl::open_stream already does and fail with
Error::Write. Add an end-to-end test asserting the client refuses a CR/LF target
instead of putting it on the wire.

---------

Co-authored-by: Sayed Kaif <metsw24@gmail.com>
2026-07-18 18:07:44 -04:00
yhirose
75938f08c7 Match chunked as the final transfer coding, order-independently (#2500)
is_chunked_transfer_encoding compared the whole Transfer-Encoding field
value against "chunked", so a message whose final coding is chunked but
which names another coding first ("gzip, chunked", valid under RFC 9112
6.1) was read as unframed. On a keep-alive server the body was then left
in the socket and parsed as a smuggled request; open_stream repeated the
check case-sensitively with a raw ==, desyncing the client stream the
same way.

Rework the helper to match the last coding token case-insensitively and
route open_stream through it so both paths agree. The codings may also be
split across multiple Transfer-Encoding lines (RFC 9110 5.3); since
Headers is an unordered_multimap whose duplicate-key iteration order is
not portable, the final coding of a multi-line field cannot be
determined reliably, so treat any such message that names chunked as
chunked (fail safe: a mis-parse only closes the connection, whereas the
opposite error enables smuggling). A unit test covers the helper,
including order-independent multi-line cases.

Based on #2487 by @metsw24-max.
2026-07-18 18:07:29 -04:00
yhirose
0c1cc8c986 Strip Cookie headers on cross-origin redirect
Cookie and Cookie2 headers were forwarded to the new host when following
a cross-origin redirect, even though Host, Proxy-Authorization, and
Authorization were already stripped. Add them to the removal list so
session cookies are not leaked to a different origin.
2026-07-12 10:45:36 -04:00
yhirose
62d899feac Release v0.50.1 v0.50.1 2026-07-11 21:37:36 -04:00
yhirose
2f986fd5e5 Fix use-after-free of TLS session in WebSocketClient::shutdown_and_close()
shutdown_and_close() freed the TLS session before ws_->close() sent the
WebSocket close frame. The WebSocket's SSLSocketStream keeps a raw pointer
to that session, so sending the close frame then read/wrote a freed SSL
object. Reorder so ws_->close()/ws_.reset() run while the session is still
alive, then free the session (GHSA-w7p7-f35j-mw7q).
2026-07-11 21:20:36 -04:00
yhirose
ed97a6edba Release v0.50.0 v0.50.0 2026-07-11 16:01:02 -04:00
yhirose
06b8b91589 Fix Response::content_length_ not reflecting body size in Logger (Fix #2488)
Server::apply_ranges computed the correct Content-Length header for
body-based responses but never updated content_length_, so the
Logger callback always saw 0. Set content_length_ to the final body
size (post-range/post-compression) alongside the header.
2026-07-11 15:52:54 -04:00
yhirose
873d701972 Fix use-after-free in SSLClient destructor with mbedTLS (Fix #2492)
SSLClient::~SSLClient() freed the TLS context before shutting down
the SSL session. mbedTLS sessions hold a raw pointer into the
context's mbedtls_ssl_config, so a live keep-alive session's
close_notify would read freed memory. Shut down the session first,
then free the context.

Add a regression test that destructs an SSLClient while a keep-alive
mbedTLS session is still open.
2026-07-11 15:27:42 -04:00
yhirose
b40937cea8 Fix README WebSocket example to match actual API (Fix #2493)
The quick preview used a nonexistent httplib::ws::Message type with
.is_text()/.data. The actual API, as shown in README-websocket.md,
uses a plain std::string with ws.read(msg).
2026-07-11 14:12:25 -04:00
yhirose
568d434e72 Fix CRLF injection in chunked response trailers
Trailer field names and values written by write_content_chunked()'s
done_with_trailer lambda were never validated, unlike every other
header output path (set_header, WebSocket handshake, client request
headers). An application reflecting untrusted input into a trailer
via DataSink::done_with_trailer() could inject CR/LF sequences and
achieve HTTP response splitting.

Skip trailer fields with invalid names or values, matching the
silent-skip behavior of set_header().
2026-07-11 13:54:02 -04:00
yhirose
0fa4912891 Fix broken relative links in cookbook docs (Fix #2490)
Cookbook body links referenced sibling pages with a bare slug
(e.g. `c14-keep-alive`). Under the pretty-URL layout each page lives
in its own directory, so these resolve against the page's own
directory and 404. Prefix them with `../` to match the convention
already used in the tour and llm-app sections.

Verified clean with `docs-gen check`.
2026-07-08 22:45:29 -04:00
Copilot
32abac3de5 Fix ambiguous Get() examples in README after new Params overload (#2486)
* Initial plan

* Add Get(path, params) overload to ClientImpl and Client; fix ambiguous test calls

* Fix clang-format style errors in test.cc

* Fix remaining ambiguous Get() calls in test_proxy.cc and test.cc

* Update README Get() examples to disambiguate Headers overload

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-05 18:43:11 -04:00
yhirose
2132205e1a Release v0.49.0 v0.49.0 2026-07-01 23:56:17 -04:00
yhirose
a4d7066c2c Log res.error() in GetWithRange test assertions to diagnose flaky CI failures 2026-07-01 23:46:05 -04:00
yhirose
bd455f8b91 Add locale-independent ASCII classification helpers (Fix #2482, Fix #2483)
std::isalnum and std::isdigit consult the global C locale, so a byte
like 0xC5 can classify as alphanumeric once an embedder calls
setlocale() (observed on macOS). HTTP grammars are defined over ASCII,
so raw bytes must be classified without regard to the locale.

Add detail::is_ascii_digit/is_ascii_alpha/is_ascii_alnum and use them
at every classification site: multipart boundary validation, token
checks, URI encoding, range header parsing, is_numeric, is_hex, and
IPv4 host detection. Also unify the hand-written digit range checks in
from_chars, URL parsing, and parse_ipv4 onto the same helpers.

With these in place nothing uses <cctype> anymore, so the include is
dropped.
2026-07-01 23:14:53 -04:00
Yanjun Yang(Pluto)
9a5321aadb meson: fix build failure on glibc >= 2.34 without standalone libanl (#2484)
On glibc >= 2.34, getaddrinfo_a is no longer provided by a standalone
libanl shared library — it is built directly into libc.  Newer
architectures (e.g. loongarch64, riscv64) have never shipped a separate
libanl, causing the meson build to fail with:

    C++ shared or static library 'anl' not found

Signed-off-by: Pluto Yang <yangyj.ee@gmail.com>
2026-07-01 22:21:44 -04:00
yhirose
32ff75e355 Make ThreadPool idle timeout configurable at runtime (Fix #2481) 2026-07-01 22:08:01 -04:00
yhirose
45da614ddd Omit default port from WebSocket handshake Host header (Fix #2480)
The WebSocket upgrade request always appended ":port" to the Host
header, violating RFC 6455 Section 4.1 which says the port should be
included only when it is not the default (80 for ws, 443 for wss).
Some CDNs alter routing when the Host header carries an explicit
default port.

Build the Host header with detail::make_host_and_port_string, which
also brackets IPv6 literal hosts correctly.
2026-07-01 21:38:57 -04:00
yhirose
f5c8c982df Escape CR and LF in multipart part content types
Same header-injection vector as the name/filename fix: item.content_type
was concatenated into the part's Content-Type header unescaped, so
embedded CR/LF could inject arbitrary part headers.

Escape CR -> %0D and LF -> %0A via escape_multipart_field with a new
escape_quote = false mode. '"' is left intact since it is legal in
Content-Type values (e.g. quoted charset parameters) and appears outside
a quoted-string context here.
2026-07-01 21:38:57 -04:00
yhirose
9e8b960e44 Add public MultipartFormDataWriter for multipart body serialization
Building a multipart/form-data body outside Client::Post/Put/Patch
(e.g. to feed a custom content provider or compose with other body
sources) previously required calling several detail:: functions
directly, which is not a stable interface.

Introduce MultipartFormDataWriter, a small public class that owns the
boundary and delegates to the existing detail:: serializers: whole-body
serialization with known content length, and per-part framing
(item_begin/item_end/finish) for streaming. Also expose
is_valid_multipart_boundary() so callers using an explicit boundary can
validate it without exceptions.
2026-07-01 21:38:57 -04:00
yhirose
8149bb38fc Escape quote, CR and LF in multipart field names and filenames
detail::serialize_multipart_formdata_item_begin() concatenated item.name
and item.filename into the Content-Disposition quoted-string without any
escaping. A '"' in either value silently terminated the quoted-string
early, and embedded CR/LF allowed injecting arbitrary part headers or
forging part boundaries when a filename comes from external input.

Escape both values following the WHATWG HTML standard ('"' -> %22,
CR -> %0D, LF -> %0A), which matches what browsers send in
multipart/form-data bodies. Behavior only changes for inputs that
previously produced malformed HTTP.
2026-07-01 21:38:57 -04:00
Saber Haj Rabiee
3fe32b63b4 Send query string verbatim when path encoding is disabled (#2479)
set_path_encode(false) only suppressed encoding of the path part. The
query part was always run through normalize_query_string(), which
decodes then re-encodes each key/value pair regardless of the flag.

That round-trip is lossy for pre-encoded payloads: re-encoding emits
sub-delimiters literally (%2C->",", %24->"$", %3B->";", ...) and turns
%20 into "+", which a strict RFC 3986 server decodes back as "+"
(0x2B) rather than a space (0x20), corrupting binary query data.

Honor path_encode_ for the query as well: when disabled, append the
caller-supplied query verbatim. Add a regression test asserting on the
raw request target, since the server decodes "+" as space and would
otherwise mask the difference.
2026-07-01 21:18:05 -04:00
yhirose
0ae93881b4 Clarify comment on base64_encode accumulator signedness 2026-07-01 20:12:44 -04:00
metsw24-max
a7b886b9cb Use an unsigned accumulator in base64_encode (#2477)
* use an unsigned accumulator in base64_encode

* Forward-declare detail::base64_encode for split builds
2026-07-01 20:12:00 -04:00
yhirose
9d159bb412 Release v0.48.0 v0.48.0 2026-06-18 20:13:57 -04:00
yhirose
9ac64e90db Generate IP-host test certificates in CMake build
The cert_ip_cn.pem and cert_ipv6.pem certificates added in ba390f2 were
only generated by gen-certs.sh, which the Makefile-based Linux/macOS CI
uses. The Windows CI builds with CMake, whose own certificate-generation
block was not updated, so cert_ipv6.pem was missing there and
SSLClientServerTest.TlsVerifyHostnameIpv6San failed on is_valid().

Mirror the two openssl commands into test/CMakeLists.txt to keep both
certificate-generation paths in sync.
2026-06-18 19:50:46 -04:00
yhirose
ba390f2399 Restrict IP-host hostname verification to iPAddress SANs on Mbed TLS and wolfSSL
An IP-literal host must only be authenticated via a matching iPAddress SAN,
never via the certificate's Common Name (RFC 9110), as the OpenSSL backend
already does through X509_check_ip. The Mbed TLS and wolfSSL backends instead
fell back to the CN when no IP SAN matched, and recognized IPv4 only.

This is a more complete solution for #2476, which gated the CN fallback for
IPv4 hosts only; here the same gap is closed for IPv6 as well, and IPv6
iPAddress SANs are actually matched.

- Add impl::parse_ip_address() to parse IPv4/IPv6 literals into raw bytes
- Match IPv6 (16-byte) iPAddress SANs, not just IPv4
- Skip the CN fallback for IP-literal hosts (both IPv4 and IPv6)
- Remove the unused SSLClient::verify_host* dead code
- Add regression tests and test certificates for the IP-host cases
2026-06-18 12:37:16 -04:00
yhirose
7307c41aa8 Better from_chars implementation (Fix #2475) 2026-06-17 23:21:36 -04:00
Arthur Chan
df0b7d243b OSS-Fuzz: Add new fuzzer targets multipart parsing (#2473)
* OSS-Fuzz: Add new fuzzer targets multipart parsing

Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>

* Fix formatting

Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>

---------

Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
2026-06-13 22:39:45 -04:00
metsw24-max
28d95937b5 use strict hex parsing in decode_query_component (#2472) 2026-06-12 13:54:04 -04:00
yhirose
7532932276 Fix OpenSSL 4.0 deprecation warnings
OpenSSL 4.0 deprecates X509_STORE_get0_objects() and
X509_NAME_get_text_by_NID(), producing warnings on the OpenSSL
backend (surfaced by Debian bug #1138434, which also reported a
const-conversion build error already fixed in v0.43.1).

- Fetch CA store objects via X509_STORE_get1_objects() (thread-safe,
  OpenSSL 3.3+) in get_ca_certs() and get_ca_names(), releasing the
  snapshot with a scope_exit guard; keep using get0 on older OpenSSL,
  BoringSSL, and LibreSSL
- Extract the subject CN in get_cert_subject_cn() with
  X509_NAME_get_index_by_NID()/X509_NAME_get_entry() instead of the
  deprecated X509_NAME_get_text_by_NID()
- Delegate SSLClient::verify_host_with_common_name() to
  tls::get_cert_subject_cn() to drop the remaining deprecated call

Verified against OpenSSL 4.0.1, 3.6.2, and 3.0: builds warning-free
and passes the full test suite.
2026-06-12 13:20:15 -04:00
yhirose
8829fac98e Use full git history in docs workflow for sitemap lastmod 2026-06-10 23:18:15 -04:00
yhirose
fe332fa06b Release v0.47.0 v0.47.0 2026-06-10 00:24:05 -04:00
yhirose
fa981cedae Fix TLS chain verification bypass for IP hosts on Mbed TLS and wolfSSL
For connections to IP-literal hosts with server certificate
verification enabled, the Mbed TLS and wolfSSL backends downgraded the
verification mode before the handshake because no hostname could be
bound for in-handshake checks:

- SSLClient skipped certificate chain validation entirely; only the
  post-handshake identity check (IP SAN match) remained, so any
  untrusted certificate carrying a matching IP SAN was accepted
- The WebSocket client skipped verification altogether on Mbed TLS,
  accepting any certificate

Keep the verification mode enabled for IP hosts and verify the
certificate identity post-handshake via tls::verify_hostname(), which
supports IP SANs on all backends. The WebSocket path now performs the
same post-handshake identity check as SSLClient. On Mbed TLS, sessions
explicitly opt out of in-handshake hostname verification (mandatory
since Mbed TLS 3.6.4) and the post-handshake check covers identity
instead; DNS hosts still bind the hostname during the handshake. Also
stop sending SNI for IP hosts on Mbed TLS and wolfSSL (RFC 6066).
2026-06-10 00:07:27 -04:00