CVE-2023-26130: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: Versions of the package yhirose/cpp-httplib before 0.12.4 are vulnerable to CRLF Injection when untrusted user input is used to set the content-type header in the HTTP .Patch, .Post, .Put and .Delete requests. This can lead to logical errors and other misbehaviors.**Note:** This issue is present due to an incomplete fix for [CVE-2020-11709](https://security.snyk.io/vuln/SNYK-UNMANAGED-YHIROSECPPHTTPLIB-2366507).
CWE description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
CVSS v3 base score: 8.8
Published: 2023-05-30T05:15Z
Repository: https://github.com/yhirose/cpp-httplib
Language: C++
Files changed: httplib.h, test/test.cc
Commit date: 2023-05-22 22:56:16 +0900
Commit message: Fix more CRLF injection problems.

Diff: diff --git a/httplib.h b/httplib.h
index 0da282b..54f37a6 100644
--- a/httplib.h
+++ b/httplib.h
@@ -5923,12 +5923,12 @@ inline void Server::apply_ranges(const Request &req, Response &res,
     if (it != res.headers.end()) {
       content_type = it->second;
       res.headers.erase(it);
     }
 
-    res.headers.emplace("Content-Type",
-                        "multipart/byteranges; boundary=" + boundary);
+    res.set_header("Content-Type",
+                   "multipart/byteranges; boundary=" + boundary);
   }
 
   auto type = detail::encoding_type(req, res);
 
   if (res.body.empty()) {
@@ -6614,61 +6614,61 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
 inline bool ClientImpl::write_request(Stream &strm, Request &req,
                                       bool close_connection, Error &error) {
   // Prepare additional headers
   if (close_connection) {
     if (!req.has_header("Connection")) {
-      req.headers.emplace("Connection", "close");
+      req.set_header("Connection", "close");
     }
   }
 
   if (!req.has_header("Host")) {
     if (is_ssl()) {
       if (port_ == 443) {
-        req.headers.emplace("Host", host_);
+        req.set_header("Host", host_);
       } else {
-        req.headers.emplace("Host", host_and_port_);
+        req.set_header("Host", host_and_port_);
       }
     } else {
       if (port_ == 80) {
-        req.headers.emplace("Host", host_);
+        req.set_header("Host", host_);
       } else {
-        req.headers.emplace("Host", host_and_port_);
+        req.set_header("Host", host_and_port_);
       }
     }
   }
 
-  if (!req.has_header("Accept")) { req.headers.emplace("Accept", "*/*"); }
+  if (!req.has_header("Accept")) { req.set_header("Accept", "*/*"); }
 
 #ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT
   if (!req.has_header("User-Agent")) {
     auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION;
-    req.headers.emplace("User-Agent", agent);
+    req.set_header("User-Agent", agent);
   }
 #endif
 
   if (req.body.empty()) {
     if (req.content_provider_) {
       if (!req.is_chunked_content_provider_) {
         if (!req.has_header("Content-Length")) {
           auto length = std::to_string(req.content_length_);
-          req.headers.emplace("Content-Length", length);
+          req.set_header("Content-Length", length);
         }
       }
     } else {
       if (req.method == "POST" || req.method == "PUT" ||
           req.method == "PATCH") {
-        req.headers.emplace("Content-Length", "0");
+        req.set_header("Content-Length", "0");
       }
     }
   } else {
     if (!req.has_header("Content-Type")) {
-      req.headers.emplace("Content-Type", "text/plain");
+      req.set_header("Content-Type", "text/plain");
     }
 
     if (!req.has_header("Content-Length")) {
       auto length = std::to_string(req.body.size());
-      req.headers.emplace("Content-Length", length);
+      req.set_header("Content-Length", length);
     }
   }
 
   if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
     if (!req.has_header("Authorization")) {
@@ -6732,16 +6732,14 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
 inline std::unique_ptr<Response> ClientImpl::send_with_content_provider(
     Request &req, const char *body, size_t content_length,
     ContentProvider content_provider,
     ContentProviderWithoutLength content_provider_without_length,
     const std::string &content_type, Error &error) {
-  if (!content_type.empty()) {
-    req.headers.emplace("Content-Type", content_type);
-  }
+  if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
 
 #ifdef CPPHTTPLIB_ZLIB_SUPPORT
-  if (compress_) { req.headers.emplace("Content-Encoding", "gzip"); }
+  if (compress_) { req.set_header("Content-Encoding", "gzip"); }
 #endif
 
 #ifdef CPPHTTPLIB_ZLIB_SUPPORT
   if (compress_ && !content_provider_without_length) {
     // TODO: Brotli support
@@ -6798,11 +6796,11 @@ inline std::unique_ptr<Response> ClientImpl::send_with_content_provider(
     } else if (content_provider_without_length) {
       req.content_length_ = 0;
       req.content_provider_ = detail::ContentProviderAdapter(
           std::move(content_provider_without_length));
       req.is_chunked_content_provider_ = true;
-      req.headers.emplace("Transfer-Encoding", "chunked");
+      req.set_header("Transfer-Encoding", "chunked");
     } else {
       req.body.assign(body, content_length);
       ;
     }
   }
@@ -7421,13 +7419,11 @@ inline Result ClientImpl::Delete(const std::string &path,
   Request req;
   req.method = "DELETE";
   req.headers = headers;
   req.path = path;
 
-  if (!content_type.empty()) {
-    req.headers.emplace("Content-Type", content_type);
-  }
+  if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
   req.body.assign(body, content_length);
 
   return send_(std::move(req));
 }
 
diff --git a/test/test.cc b/test/test.cc
index 59cf04b..b57bb0b 100644
--- a/test/test.cc
+++ b/test/test.cc
@@ -6114,5 +6114,51 @@ TEST(RedirectTest, RedirectToUrlWithQueryParameters) {
     ASSERT_TRUE(res);
     EXPECT_EQ(200, res->status);
     EXPECT_EQ("val&key2=val2", res->body);
   }
 }
+
+TEST(VulnerabilityTest, CRLFInjection) {
+  Server svr;
+
+  svr.Post("/test1", [](const Request &/*req*/, Response &res) {
+    res.set_content("Hello 1", "text/plain");
+  });
+
+  svr.Delete("/test2", [](const Request &/*req*/, Response &res) {
+    res.set_content("Hello 2", "text/plain");
+  });
+
+  svr.Put("/test3", [](const Request &/*req*/, Response &res) {
+    res.set_content("Hello 3", "text/plain");
+  });
+
+  svr.Patch("/test4", [](const Request &/*req*/, Response &res) {
+    res.set_content("Hello 4", "text/plain");
+  });
+
+  svr.set_logger([](const Request &req, const Response & /*res*/) {
+    for (const auto &x : req.headers) {
+      auto key = x.first;
+      EXPECT_STRNE("evil", key.c_str());
+    }
+  });
+
+  auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    thread.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+
+  std::this_thread::sleep_for(std::chrono::seconds(1));
+
+  {
+    Client cli(HOST, PORT);
+
+    cli.Post("/test1", "A=B",
+             "application/x-www-form-urlencoded\r\nevil: hello1");
+    cli.Delete("/test2", "A=B", "text/plain\r\nevil: hello2");
+    cli.Put("/test3", "text", "text/plain\r\nevil: hello3");
+    cli.Patch("/test4", "content", "text/plain\r\nevil: hello4");
+  }
+}

Vulnerable code:     res.headers.emplace("Content-Type",
                        "multipart/byteranges; boundary=" + boundary);
      req.headers.emplace("Connection", "close");
        req.headers.emplace("Host", host_);
        req.headers.emplace("Host", host_and_port_);
        req.headers.emplace("Host", host_);
        req.headers.emplace("Host", host_and_port_);
  if (!req.has_header("Accept")) { req.headers.emplace("Accept", "*/*"); }
    req.headers.emplace("User-Agent", agent);
          req.headers.emplace("Content-Length", length);
        req.headers.emplace("Content-Length", "0");
      req.headers.emplace("Content-Type", "text/plain");
      req.headers.emplace("Content-Length", length);
  if (!content_type.empty()) {
    req.headers.emplace("Content-Type", content_type);
  }
  if (compress_) { req.headers.emplace("Content-Encoding", "gzip"); }
      req.headers.emplace("Transfer-Encoding", "chunked");
  if (!content_type.empty()) {
    req.headers.emplace("Content-Type", content_type);
  }
Fixed code:     res.set_header("Content-Type",
                   "multipart/byteranges; boundary=" + boundary);
      req.set_header("Connection", "close");
        req.set_header("Host", host_);
        req.set_header("Host", host_and_port_);
        req.set_header("Host", host_);
        req.set_header("Host", host_and_port_);
  if (!req.has_header("Accept")) { req.set_header("Accept", "*/*"); }
    req.set_header("User-Agent", agent);
          req.set_header("Content-Length", length);
        req.set_header("Content-Length", "0");
      req.set_header("Content-Type", "text/plain");
      req.set_header("Content-Length", length);
  if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
  if (compress_) { req.set_header("Content-Encoding", "gzip"); }
      req.set_header("Transfer-Encoding", "chunked");
  if (!content_type.empty()) { req.set_header("Content-Type", content_type); }

TEST(VulnerabilityTest, CRLFInjection) {
  Server svr;

  svr.Post("/test1", [](const Request &/*req*/, Response &res) {
    res.set_content("Hello 1", "text/plain");
  });

  svr.Delete("/test2", [](const Request &/*req*/, Response &res) {
    res.set_content("Hello 2", "text/plain");
  });

  svr.Put("/test3", [](const Request &/*req*/, Response &res) {
    res.set_content("Hello 3", "text/plain");
  });

  svr.Patch("/test4", [](const Request &/*req*/, Response &res) {
    res.set_content("Hello 4", "text/plain");
  });

  svr.set_logger([](const Request &req, const Response & /*res*/) {
    for (const auto &x : req.headers) {
      auto key = x.first;
      EXPECT_STRNE("evil", key.c_str());
    }
  });

  auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
  auto se = detail::scope_exit([&] {
    svr.stop();
    thread.join();
    ASSERT_FALSE(svr.is_running());
  });

  std::this_thread::sleep_for(std::chrono::seconds(1));

  {
    Client cli(HOST, PORT);

    cli.Post("/test1", "A=B",
             "application/x-www-form-urlencoded\r\nevil: hello1");
    cli.Delete("/test2", "A=B", "text/plain\r\nevil: hello2");
    cli.Put("/test3", "text", "text/plain\r\nevil: hello3");
    cli.Patch("/test4", "content", "text/plain\r\nevil: hello4");
  }
}