VYPR
High severityNVD Advisory· Published May 29, 2026· Updated May 29, 2026

CVE-2026-46527

CVE-2026-46527

Description

cpp-httplib is a C++11 single-file header-only cross platform HTTP/HTTPS library. Prior to 0.44.0, When the server has called Server::set_trusted_proxies() with a non-empty trusted-proxy list, an attacker can send an HTTP request that includes an X-Forwarded-For header whose value parses to no valid IP segments. The code path then executes get_client_ip(), which calls front() on an empty std::vector—undefined behavior in C++. On typical implementations this manifests as abnormal process termination (denial of service). With Sanitizers enabled, you get an explicit runtime diagnostic. This vulnerability is fixed in 0.44.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

cpp-httplib prior to 0.44.0 crashes when processing a malicious X-Forwarded-For header under trusted proxy configuration.

Vulnerability

In cpp-httplib versions prior to 0.44.0, when the server has called Server::set_trusted_proxies() with a non-empty trusted-proxy list, an attacker can send an HTTP request that includes an X-Forwarded-For header whose value parses to no valid IP segments. The code path then executes get_client_ip(), which calls front() on an empty std::vector—undefined behavior in C++. On typical implementations this manifests as abnormal process termination (denial of service). [1]

Exploitation

An attacker with network access to the server can send a crafted HTTP request containing an X-Forwarded-For header with an empty or malformed value (e.g., X-Forwarded-For: ). No authentication is required. The server, if configured with a non-empty trusted proxy list via set_trusted_proxies(), will parse the header and attempt to extract the client IP, leading to the undefined behavior. [1]

Impact

Successful exploitation results in a denial of service due to abnormal process termination. The server crashes, causing service disruption. No information disclosure, privilege escalation, or remote code execution is possible. [1]

Mitigation

The vulnerability is fixed in version 0.44.0. Users should upgrade to this version or later. No workarounds are documented. The CVE is not listed on the CISA Known Exploited Vulnerabilities catalog. [1]

AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Yhirose/Cpp Httplibinferred2 versions
    <0.44.0+ 1 more
    • (no CPE)range: <0.44.0
    • (no CPE)range: <0.44.0

Patches

3
fbb031ed8504

Stop percent-decoding HTTP request header values

https://github.com/yhirose/cpp-httplibyhiroseMay 10, 2026Fixed in 0.44.0via llm-release-walk
2 files changed · +121 6
  • httplib.h+5 6 modified
    @@ -5016,12 +5016,11 @@ inline bool parse_header(const char *beg, const char *end, T fn) {
     
         if (!detail::fields::is_field_value(val)) { return false; }
     
    -    if (case_ignore::equal(key, "Location") ||
    -        case_ignore::equal(key, "Referer")) {
    -      fn(key, val);
    -    } else {
    -      fn(key, decode_path_component(val));
    -    }
    +    // RFC 9110 §5.5: header field values are opaque octets and MUST NOT be
    +    // percent-decoded by the recipient. Applications that need to interpret a
    +    // value as a URI component should call httplib::decode_uri_component()
    +    // (or decode_path_component()) explicitly.
    +    fn(key, val);
     
         return true;
       }
    
  • test/test.cc+116 0 modified
    @@ -7441,6 +7441,122 @@ TEST(ServerRequestParsingTest, EmptyFieldValue) {
       EXPECT_EQ("HTTP/1.1 200 OK", out.substr(0, 15));
     }
     
    +TEST(ServerRequestParsingTest, HeaderValueNotPercentDecoded) {
    +  Server svr;
    +  std::string x_custom;
    +  std::string cookie;
    +  std::string xff;
    +  std::string x_unicode;
    +  std::string x_iis;
    +
    +  svr.Get("/check", [&](const Request &req, Response &res) {
    +    x_custom = req.get_header_value("X-Custom");
    +    cookie = req.get_header_value("Cookie");
    +    xff = req.get_header_value("X-Forwarded-For");
    +    x_unicode = req.get_header_value("X-Unicode");
    +    x_iis = req.get_header_value("X-IIS");
    +    res.set_content("ok", "text/plain");
    +  });
    +
    +  thread t = thread([&] { svr.listen(HOST, PORT); });
    +  auto se = detail::scope_exit([&] {
    +    svr.stop();
    +    t.join();
    +    ASSERT_FALSE(svr.is_running());
    +  });
    +
    +  svr.wait_until_ready();
    +
    +  const std::string req = "GET /check HTTP/1.1\r\n"
    +                          "Host: localhost\r\n"
    +                          "X-Custom: a%0D%0AInjected: b\r\n"
    +                          "Cookie: session%3Dvictim%3B%20admin%3Dyes\r\n"
    +                          "X-Forwarded-For: 1.2.3.4%2C5.6.7.8\r\n"
    +                          "X-Unicode: %E3%81%82\r\n"
    +                          "X-IIS: %u00E9\r\n"
    +                          "Connection: close\r\n"
    +                          "\r\n";
    +
    +  std::string res;
    +  ASSERT_TRUE(send_request(5, req, &res));
    +  EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
    +
    +  // Every value must be returned verbatim (wire form), with no decoding.
    +  EXPECT_EQ("a%0D%0AInjected: b", x_custom);
    +  EXPECT_EQ("session%3Dvictim%3B%20admin%3Dyes", cookie);
    +  EXPECT_EQ("1.2.3.4%2C5.6.7.8", xff);
    +  EXPECT_EQ("%E3%81%82", x_unicode);
    +  EXPECT_EQ("%u00E9", x_iis);
    +}
    +
    +// Applications that previously relied on automatic percent-decoding can
    +// reproduce the old behavior by explicitly calling decode_path_component()
    +// or, for RFC 3986 conformance, decode_uri_component().
    +TEST(ServerRequestParsingTest, HeaderValueExplicitDecodingByApplication) {
    +  Server svr;
    +  std::string decoded;
    +
    +  svr.Get("/check", [&](const Request &req, Response &res) {
    +    decoded = decode_uri_component(req.get_header_value("X-Custom"));
    +    res.set_content("ok", "text/plain");
    +  });
    +
    +  thread t = thread([&] { svr.listen(HOST, PORT); });
    +  auto se = detail::scope_exit([&] {
    +    svr.stop();
    +    t.join();
    +    ASSERT_FALSE(svr.is_running());
    +  });
    +
    +  svr.wait_until_ready();
    +
    +  const std::string req = "GET /check HTTP/1.1\r\n"
    +                          "Host: localhost\r\n"
    +                          "X-Custom: hello%20world\r\n"
    +                          "Connection: close\r\n"
    +                          "\r\n";
    +
    +  std::string res;
    +  ASSERT_TRUE(send_request(5, req, &res));
    +  EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
    +  EXPECT_EQ("hello world", decoded);
    +}
    +
    +// Regression test for #2033. Browsers send Referer values that include
    +// percent-encoded characters such as %0A inside the URL. Decoding the
    +// header value would either trip the post-decode CR/LF/NUL guard (the
    +// original bug, returning 400) or, after that guard was relaxed, silently
    +// store a literal LF — both unacceptable. The wire form must round-trip.
    +TEST(ServerRequestParsingTest, RefererWithPercentEncodedNewline) {
    +  Server svr;
    +  std::string referer;
    +
    +  svr.Get("/check", [&](const Request &req, Response &res) {
    +    referer = req.get_header_value("Referer");
    +    res.set_content("ok", "text/plain");
    +  });
    +
    +  thread t = thread([&] { svr.listen(HOST, PORT); });
    +  auto se = detail::scope_exit([&] {
    +    svr.stop();
    +    t.join();
    +    ASSERT_FALSE(svr.is_running());
    +  });
    +
    +  svr.wait_until_ready();
    +
    +  const std::string req = "GET /check HTTP/1.1\r\n"
    +                          "Host: localhost\r\n"
    +                          "Referer: http://localhost:1111/?q=Hello%0A\r\n"
    +                          "Connection: close\r\n"
    +                          "\r\n";
    +
    +  std::string res;
    +  ASSERT_TRUE(send_request(5, req, &res));
    +  EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
    +  EXPECT_EQ("http://localhost:1111/?q=Hello%0A", referer);
    +}
    +
     TEST(ServerStopTest, StopServerWithChunkedTransmission) {
       Server svr;
     
    
811dd0b6f238

Release v0.44.0

https://github.com/yhirose/cpp-httplibyhiroseMay 10, 2026Fixed in 0.44.0via release-tag
2 files changed · +3 3
  • docs-src/config.toml+1 1 modified
    @@ -4,7 +4,7 @@ langs = ["en", "ja"]
     
     [site]
     title = "cpp-httplib"
    -version = "0.43.4"
    +version = "0.44.0"
     hostname = "https://yhirose.github.io"
     base_path = "/cpp-httplib"
     footer_message = "© 2026 Yuji Hirose. All rights reserved."
    
  • httplib.h+2 2 modified
    @@ -8,8 +8,8 @@
     #ifndef CPPHTTPLIB_HTTPLIB_H
     #define CPPHTTPLIB_HTTPLIB_H
     
    -#define CPPHTTPLIB_VERSION "0.43.4"
    -#define CPPHTTPLIB_VERSION_NUM "0x002b04"
    +#define CPPHTTPLIB_VERSION "0.44.0"
    +#define CPPHTTPLIB_VERSION_NUM "0x002c00"
     
     #ifdef _WIN32
     #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
    
7d5082cc0e6e

Make ThreadPool ctor exception-safe on partial thread creation (#2445)

https://github.com/yhirose/cpp-httplibyhiroseMay 10, 2026Fixed in 0.44.0via llm-release-walk
3 files changed · +97 3
  • httplib.h+22 2 modified
    @@ -10047,9 +10047,29 @@ inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr)
     #endif
       max_thread_count_ = max_n == 0 ? n : max_n;
       threads_.reserve(base_thread_count_);
    -  for (size_t i = 0; i < base_thread_count_; i++) {
    -    threads_.emplace_back(std::thread([this]() { worker(false); }));
    +#ifndef CPPHTTPLIB_NO_EXCEPTIONS
    +  try {
    +#endif
    +    for (size_t i = 0; i < base_thread_count_; i++) {
    +      threads_.emplace_back(std::thread([this]() { worker(false); }));
    +    }
    +#ifndef CPPHTTPLIB_NO_EXCEPTIONS
    +  } catch (...) {
    +    // If thread creation fails partway (e.g., pthread_create returns EAGAIN),
    +    // signal the workers we already spawned to exit and join them so the
    +    // vector destructor does not see joinable threads (which would call
    +    // std::terminate). Then rethrow so the caller learns of the failure.
    +    {
    +      std::unique_lock<std::mutex> lock(mutex_);
    +      shutdown_ = true;
    +    }
    +    cond_.notify_all();
    +    for (auto &t : threads_) {
    +      if (t.joinable()) { t.join(); }
    +    }
    +    throw;
       }
    +#endif
     }
     
     inline bool ThreadPool::enqueue(std::function<void()> fn) {
    
  • test/Makefile+18 1 modified
    @@ -202,8 +202,25 @@ test_split_no_tls : test.cc ../httplib.h httplib.cc Makefile
     	$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_NO_TLS)
     
     # ThreadPool unit tests (no TLS, no compression needed)
    +#
    +# The constructor-exception-safety reproducer test interposes pthread_create
    +# at link time. The link flags below enable that interposition. ASAN is also
    +# stripped from this target because libasan installs its own pthread_create
    +# interceptor; layering our override on top corrupts ASAN's thread bookkeeping
    +# and trips "Joining already joined thread" on Linux. ThreadPool memory
    +# behavior is still covered by the ASAN-instrumented `test` binary.
    +ifneq ($(OS), Windows_NT)
    +ifeq ($(shell uname -s), Darwin)
    +THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,-flat_namespace
    +else
    +THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,--export-dynamic
    +endif
    +endif
    +
    +THREAD_POOL_CXXFLAGS := $(filter-out -fsanitize=address,$(CXXFLAGS))
    +
     test_thread_pool : test_thread_pool.cc ../httplib.h Makefile
    -	$(CXX) -o $@ -I.. $(CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread
    +	$(CXX) -o $@ -I.. $(THREAD_POOL_CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread $(THREAD_POOL_INTERPOSE_LDFLAGS)
     
     check_abi:
     	@./check-shared-library-abi-compatibility.sh
    
  • test/test_thread_pool.cc+57 0 modified
    @@ -198,6 +198,63 @@ TEST(ThreadPoolTest, InvalidMaxThreadsThrows) {
     }
     #endif
     
    +// Issue #2444: ThreadPool constructor must be exception-safe when std::thread
    +// construction fails partway (e.g., pthread_create returns EAGAIN under thread
    +// resource pressure). Without proper handling, the partially-built threads_
    +// vector destroys joinable std::thread objects, calling std::terminate().
    +//
    +// We reproduce the failure portably by interposing pthread_create at link
    +// time: while the counter is armed, the first N calls succeed, the rest
    +// return EAGAIN. This is gated to POSIX + exceptions-enabled builds.
    +#ifndef CPPHTTPLIB_NO_EXCEPTIONS
    +#if defined(__unix__) || defined(__APPLE__)
    +
    +#include <dlfcn.h>
    +#include <errno.h>
    +#include <pthread.h>
    +
    +namespace {
    +// -1 = pass-through (default). >= 0 = number of remaining successful calls
    +// before EAGAIN is returned. Reset to -1 after each test that arms it.
    +std::atomic<int> g_pthread_create_remaining{-1};
    +} // namespace
    +
    +extern "C" int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    +                              void *(*start_routine)(void *), void *arg) {
    +  using fn_t =
    +      int (*)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *);
    +  static fn_t real = reinterpret_cast<fn_t>(dlsym(RTLD_NEXT, "pthread_create"));
    +
    +  int n = g_pthread_create_remaining.load(std::memory_order_relaxed);
    +  if (n == 0) { return EAGAIN; }
    +  if (n > 0) {
    +    g_pthread_create_remaining.fetch_sub(1, std::memory_order_relaxed);
    +  }
    +  return real(thread, attr, start_routine, arg);
    +}
    +
    +TEST(ThreadPoolTest, ConstructorRecoversWhenThreadCreationFails) {
    +  // Allow only the first thread to spawn; subsequent pthread_create calls
    +  // return EAGAIN, causing std::thread() to throw std::system_error mid-loop.
    +  g_pthread_create_remaining.store(1);
    +
    +  bool caught = false;
    +  try {
    +    ThreadPool pool(/*n=*/4);
    +    (void)pool;
    +  } catch (const std::system_error &) { caught = true; } catch (...) {
    +    caught = true;
    +  }
    +
    +  // Disarm before any further test runs.
    +  g_pthread_create_remaining.store(-1);
    +
    +  EXPECT_TRUE(caught);
    +}
    +
    +#endif // POSIX
    +#endif // CPPHTTPLIB_NO_EXCEPTIONS
    +
     TEST(ThreadPoolTest, EnqueueAfterShutdownReturnsFalse) {
       ThreadPool pool(2);
       pool.shutdown();
    

Vulnerability mechanics

Root cause

"Missing emptiness check before calling `front()` on a `std::vector` that may be empty when `X-Forwarded-For` parses to zero IP segments."

Attack vector

An attacker sends an HTTP request containing an `X-Forwarded-For` header whose value is empty, comma-only (e.g. `,`), or whitespace-only to a server that has called `set_trusted_proxies()` with a non-empty list [ref_id=1]. The header is accepted because `is_field_content` returns true for empty strings [ref_id=1]. The `detail::split` function then trims each segment and only invokes the callback when the trimmed segment is non-empty, so no IPs are pushed into `ip_list` [ref_id=1]. The subsequent call to `ip_list.front()` on the empty vector triggers undefined behavior, typically causing a crash (denial of service) [CWE-476, derived from ref_id=1's description of undefined behavior and crash].

Affected code

The vulnerability is in `httplib.h` inside `get_client_ip()` (approx. lines 11879–11911). When `Server::process_request` (approx. lines 11979–11984) calls `get_client_ip()` with an `X-Forwarded-For` header that parses to zero IP segments, the function reaches `return ip_list.front();` on an empty `std::vector`, which is undefined behavior. The `detail::split` function (approx. lines 5173–5193) and `parse_header` (approx. lines 4986–5025) allow empty or comma-only header values to reach this code path.

What the fix does

The patch [patch_id=3107112] adds a guard inside `get_client_ip()` to check whether `ip_list` is empty before calling `front()`. When the list is empty, the function now returns an empty string instead of invoking undefined behavior. Additionally, the caller in `process_request` is updated to only overwrite `req.remote_addr` with the result from `get_client_ip()` when that result is non-empty, falling back to the connection-level `remote_addr` otherwise. This ensures that a malformed `X-Forwarded-For` header cannot cause a crash or corrupt the request state.

Preconditions

  • configThe server must have called Server::set_trusted_proxies() with a non-empty list of trusted proxy IPs.
  • networkThe attacker must be able to send an HTTP request to the server with a crafted X-Forwarded-For header.
  • authNo authentication or prior knowledge is required.

Reproduction

Start the PoC server (which calls `set_trusted_proxies`). Send a request with `X-Forwarded-For: ,` (comma-only) using curl: `curl -sS -D- "http://127.0.0.1:18081/" -H "X-Forwarded-For: ,"`. Without sanitizers the server process typically exits abnormally and curl reports `Empty reply from server`. With ASan+UBSan, the server prints a runtime error about accessing an empty container on stderr before aborting [ref_id=1].

Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.