Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package libupnp for openSUSE:Factory checked 
in at 2026-09-12 21:15:37
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libupnp (Old)
 and      /work/SRC/openSUSE:Factory/.libupnp.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "libupnp"

Sat Sep 12 21:15:37 2026 rev:50 rq:1377378 version:22.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/libupnp/libupnp.changes  2026-08-15 
22:40:18.554917101 +0200
+++ /work/SRC/openSUSE:Factory/.libupnp.new.1265/libupnp.changes        
2026-09-12 21:16:14.762709941 +0200
@@ -1,0 +2,6 @@
+Fri Sep 11 10:23:19 UTC 2026 - Jan Engelhardt <[email protected]>
+
+- Update to release 22.1.0
+  * OSS-Fuzz: Add new fuzzer targets service table processing
+
+-------------------------------------------------------------------

Old:
----
  libupnp-22.0.6.tar.bz2

New:
----
  libupnp-22.1.0.tar.bz2

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ libupnp.spec ++++++
--- /var/tmp/diff_new_pack.lFQZtF/_old  2026-09-12 21:16:15.593744502 +0200
+++ /var/tmp/diff_new_pack.lFQZtF/_new  2026-09-12 21:16:15.595744585 +0200
@@ -19,7 +19,7 @@
 
 %define pnpver 22
 Name:           libupnp
-Version:        22.0.6
+Version:        22.1.0
 Release:        0
 Summary:        An implementation of Universal Plug and Play (UPnP)
 License:        BSD-3-Clause

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.lFQZtF/_old  2026-09-12 21:16:15.646746707 +0200
+++ /var/tmp/diff_new_pack.lFQZtF/_new  2026-09-12 21:16:15.652746956 +0200
@@ -1,5 +1,5 @@
-mtime: 1786608334
-commit: 333233cbdac79e094a1767518106d4d755f3b5c2837b893456137867b1858393
+mtime: 1789122216
+commit: 7620613e0282ae9d953b9d7c029340a392d984d6b2028c5fdf9a81699046b79f
 url: https://src.opensuse.org/jengelh/libupnp
 revision: master
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-09-11 12:23:36.000000000 +0200
@@ -0,0 +1 @@
+.osc

++++++ libupnp-22.0.6.tar.bz2 -> libupnp-22.1.0.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/CMakeLists.txt 
new/libupnp-22.1.0/CMakeLists.txt
--- old/libupnp-22.0.6/CMakeLists.txt   2026-08-12 21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/CMakeLists.txt   2026-09-10 22:47:00.000000000 +0200
@@ -7,7 +7,7 @@
 set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 
 project(PUPNP
-       VERSION 22.0.6
+       VERSION 22.1.0
        LANGUAGES C)
 
 include(GNUInstallDirs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/ChangeLog new/libupnp-22.1.0/ChangeLog
--- old/libupnp-22.0.6/ChangeLog        2026-08-12 21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/ChangeLog        2026-09-10 22:47:00.000000000 +0200
@@ -1,4 +1,32 @@
 *******************************************************************************
+Version 22.1.0
+*******************************************************************************
+
+- GHSA-f86h-hm8r-cqxr: Fix unbounded HTTP header block allowing pre-auth
+  denial of service. g_maxContentLength constrains only the entity, so the
+  request/response line and the header fields that follow it were accepted
+  with no size limit at all: a single unauthenticated request could carry an
+  unbounded number of headers, or one unbounded header value, and drive
+  memory and CPU exhaustion before any handler ran. Header insertion also
+  scans the existing list for a duplicate name, making the cost quadratic in
+  header count — 100,000 small headers occupied a worker for ~34 s and ~20 MB,
+  and a single 64 MB header value for ~34 s and ~131 MB. Both device hosts and
+  control points were affected, since the same parser reads responses.
+  Fixed by bounding the header block in parser_append(), gated on the parser
+  still reading the request/response line or the header fields, so chunked
+  trailer headers remain governed by g_maxContentLength. The limit is
+  deliberately separate from the entity limit, defaults to
+  DEFAULT_MAX_HEADER_SIZE = 16 KB, is adjustable with the new
+  UpnpSetMaxHeaderSize(), and rejects with HTTP 431 Request Header Fields Too
+  Large. Reported by the Kimi Security Team (Moonshot AI).
+- Size-limit rejections are now logged at UPNP_ERROR level naming the limit
+  that was hit, the observed sizes and the API that adjusts it
+  (UpnpSetMaxHeaderSize() or UpnpSetMaxContentLength()). Previously such a
+  rejection reached the caller only as UPNP_E_OUTOF_BOUNDS, with nothing to
+  search for. Only sizes are logged, never message bytes.
+
+
+*******************************************************************************
 Version 22.0.6
 *******************************************************************************
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/docs/Doxyfile 
new/libupnp-22.1.0/docs/Doxyfile
--- old/libupnp-22.0.6/docs/Doxyfile    2026-08-12 21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/docs/Doxyfile    2026-09-10 22:47:00.000000000 +0200
@@ -38,7 +38,7 @@
 # could be handy for archiving the generated documentation or if some version
 # control system is used.
 
-PROJECT_NUMBER         = 22.0.6
+PROJECT_NUMBER         = 22.1.0
 
 # Using the PROJECT_BRIEF tag one can provide an optional one line description
 # for a project that appears at the top of each page and should give viewer a
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/fuzzer/CMakeLists.txt 
new/libupnp-22.1.0/fuzzer/CMakeLists.txt
--- old/libupnp-22.0.6/fuzzer/CMakeLists.txt    2026-08-12 21:42:59.000000000 
+0200
+++ new/libupnp-22.1.0/fuzzer/CMakeLists.txt    2026-09-10 22:47:00.000000000 
+0200
@@ -3,6 +3,7 @@
 project(fuzzer LANGUAGES C)
 add_executable(FuzzIxml FuzzIxml.c)
 add_executable(FuzzHttp FuzzHttp.c)
+add_executable(FuzzServiceTable FuzzServiceTable.c)
 
 if(NOT DEFINED LOCAL_RUN)
        message("LOCAL_RUN is not defined")
@@ -11,6 +12,8 @@
        target_link_libraries(FuzzIxml ixml_static ${LIB_FUZZING_ENGINE})
        target_include_directories(FuzzHttp PRIVATE 
${CMAKE_SOURCE_DIR}/upnp/src/threadutil)
        target_link_libraries(FuzzHttp upnp_static ${LIB_FUZZING_ENGINE})
+       target_include_directories(FuzzServiceTable PRIVATE 
${CMAKE_SOURCE_DIR}/upnp/src/threadutil)
+       target_link_libraries(FuzzServiceTable upnp_static 
${LIB_FUZZING_ENGINE})
 else()
        message("LOCAL_RUN is ${LOCAL_RUN}")
 
@@ -33,4 +36,11 @@
                ${LIB_IXML_INCLUDE})
        target_link_libraries(FuzzHttp PRIVATE
                ${LIB_UPNP} ${LIB_IXML} ${LIB_FUZZING_ENGINE})
+
+       target_include_directories(FuzzServiceTable PUBLIC
+               ${LIB_UPNP_INCLUDE}
+               ${LIB_UPNP_SRC_INCLUDE}
+               ${LIB_IXML_INCLUDE})
+       target_link_libraries(FuzzServiceTable PRIVATE
+               ${LIB_UPNP} ${LIB_IXML} ${LIB_FUZZING_ENGINE})
 endif()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/fuzzer/FuzzServiceTable.c 
new/libupnp-22.1.0/fuzzer/FuzzServiceTable.c
--- old/libupnp-22.0.6/fuzzer/FuzzServiceTable.c        1970-01-01 
01:00:00.000000000 +0100
+++ new/libupnp-22.1.0/fuzzer/FuzzServiceTable.c        2026-09-10 
22:47:00.000000000 +0200
@@ -0,0 +1,59 @@
+#include "service_table.h"
+
+/* "ixml.h" is not included here on purpose: service_table.h already includes
+ * it, together with the other headers this target needs (config.h, upnp.h,
+ * LinkedList.h). */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+extern int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
+{
+/* service_table, getServiceTable() and freeServiceTable() are only declared
+ * when the device APIs and GENA are compiled in. Without this guard the
+ * target fails to build on a client-only configuration, for instance
+ * cmake -DFUZZER=ON -DUPNP_ENABLE_DEVICE_API=OFF. */
+#if defined(INCLUDE_DEVICE_APIS) && EXCLUDE_GENA == 0
+       IXML_Document *doc = NULL;
+       service_table table;
+       char *xml;
+
+       /* The upper bound is a harness-side safety net only. The input size is
+        * meant to be driven by the runner, through libFuzzer's -max_len flag:
+        * on the command line, as in
+        *     ./FuzzServiceTable corpus/ -max_len=65536
+        * or, under OSS-Fuzz, through a FuzzServiceTable.options file holding
+        *     [libfuzzer]
+        *     max_len = 65536
+        * Neither is set today, so libFuzzer's own default of 4096 bytes
+        * applies and the test below never actually fires. */
+       if (Size < 1 || Size > 65536) {
+               return 0;
+       }
+
+       xml = malloc(Size + 1);
+       if (!xml) {
+               return 0;
+       }
+       memcpy(xml, Data, Size);
+       xml[Size] = '\0';
+
+       /* A control point parses the device description document fetched from a
+        * device, then builds the service table from it. */
+       if (ixmlParseBufferEx(xml, &doc) == IXML_SUCCESS && doc) {
+               memset(&table, 0, sizeof(table));
+               getServiceTable((IXML_Node *)doc, &table, "http://127.0.0.1/";);
+               freeServiceTable(&table);
+               ixmlDocument_free(doc);
+       }
+
+       free(xml);
+#else
+       (void)Data;
+       (void)Size;
+#endif
+
+       return 0;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/gtest/test_httpreadwrite.cpp 
new/libupnp-22.1.0/gtest/test_httpreadwrite.cpp
--- old/libupnp-22.0.6/gtest/test_httpreadwrite.cpp     2026-08-12 
21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/gtest/test_httpreadwrite.cpp     2026-09-10 
22:47:00.000000000 +0200
@@ -16,6 +16,7 @@
 #include "gtest/gtest.h"
 #include <cstddef>
 #include <cstring>
+#include <string>
 
 extern "C" {
 #include "httpparser.h"
@@ -444,6 +445,273 @@
        EXPECT_EQ(ret, UPNP_E_SUCCESS);
 }
 
+// regression: GHSA-f86h-hm8r-cqxr
+// The HTTP header block was accepted with no size limit at all:
+// g_maxContentLength constrains only the entity, so a single unauthenticated
+// request could carry an unbounded number of headers, or one unbounded header
+// value, and drive memory and CPU exhaustion before any handler ran. Header
+// insertion also scans the existing list for a duplicate name, so cost was
+// quadratic in header count.
+//
+// Fixed by bounding the header block in parser_append(), gated on the parser
+// still reading the request/response line or the header fields.
+class GhsaF86hTestSuite : public ::testing::Test
+{
+protected:
+       int sv[2]{-1, -1};
+       size_t saved_header_limit_{};
+       size_t saved_content_limit_{};
+
+       void SetUp() override
+       {
+               saved_header_limit_ = g_maxHeaderSize;
+               saved_content_limit_ = g_maxContentLength;
+               g_maxHeaderSize = 1024; /* 1 KB header block */
+               ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
+       }
+
+       void TearDown() override
+       {
+               g_maxHeaderSize = saved_header_limit_;
+               g_maxContentLength = saved_content_limit_;
+               if (sv[0] >= 0)
+                       close(sv[0]);
+               if (sv[1] >= 0)
+                       close(sv[1]);
+       }
+};
+
+// Many small headers whose combined size exceeds the limit must be rejected.
+// Before the fix this was accepted, and the per-header duplicate scan made the
+// cost quadratic in the number of headers.
+TEST_F(GhsaF86hTestSuite, RejectsHeaderBlockExceedingLimitByCount)
+{
+       std::string req = "GET /desc.xml HTTP/1.1\r\n"
+                         "HOST: 127.0.0.1:49152\r\n";
+       /* ~14 bytes each, comfortably past the 1 KB limit */
+       for (int i = 0; i < 200; i++)
+               req += "X-Pad-" + std::to_string(i) + ": v\r\n";
+       req += "\r\n";
+       ASSERT_GT(req.size(), g_maxHeaderSize);
+
+       write(sv[1], req.data(), req.size());
+       close(sv[1]);
+       sv[1] = -1;
+
+       SOCKINFO info{};
+       info.socket = sv[0];
+
+       http_parser_t parser{};
+       int timeout = 5;
+       int http_err = 0;
+       int ret = http_RecvMessage(
+               &info, &parser, HTTPMETHOD_UNKNOWN, &timeout, &http_err);
+
+       httpmsg_destroy(&parser.msg);
+
+       EXPECT_EQ(ret, UPNP_E_OUTOF_BOUNDS);
+       EXPECT_EQ(http_err, HTTP_REQ_HEADER_FIELDS_TOO_LARGE);
+}
+
+// A single header line longer than the limit must be rejected while it is
+// still arriving. This is the case a per-header counter inside the
+// parser_parse_headers() loop cannot catch: until the line terminates, match()
+// returns PARSE_INCOMPLETE and the loop body never runs, so the limit has to 
be
+// enforced in parser_append(). The terminating CRLF is deliberately never
+// sent — rejection must happen on size alone, not at end of message.
+TEST_F(GhsaF86hTestSuite, RejectsSingleOversizedHeaderLine)
+{
+       std::string req = "GET /desc.xml HTTP/1.1\r\n"
+                         "HOST: 127.0.0.1:49152\r\n"
+                         "X-Big: ";
+       req.append(4096, 'A'); /* no CRLF, no end of headers */
+
+       write(sv[1], req.data(), req.size());
+       close(sv[1]);
+       sv[1] = -1;
+
+       SOCKINFO info{};
+       info.socket = sv[0];
+
+       http_parser_t parser{};
+       int timeout = 5;
+       int http_err = 0;
+       int ret = http_RecvMessage(
+               &info, &parser, HTTPMETHOD_UNKNOWN, &timeout, &http_err);
+
+       httpmsg_destroy(&parser.msg);
+
+       EXPECT_EQ(ret, UPNP_E_OUTOF_BOUNDS);
+       EXPECT_EQ(http_err, HTTP_REQ_HEADER_FIELDS_TOO_LARGE);
+}
+
+// The same parser handles responses, so a control point talking to a malicious
+// server must be protected too.
+TEST_F(GhsaF86hTestSuite, RejectsOversizedHeaderBlockOnResponse)
+{
+       std::string resp = "HTTP/1.1 200 OK\r\n";
+       for (int i = 0; i < 200; i++)
+               resp += "X-Pad-" + std::to_string(i) + ": v\r\n";
+       resp += "\r\n";
+       ASSERT_GT(resp.size(), g_maxHeaderSize);
+
+       write(sv[1], resp.data(), resp.size());
+       close(sv[1]);
+       sv[1] = -1;
+
+       SOCKINFO info{};
+       info.socket = sv[0];
+
+       http_parser_t parser{};
+       int timeout = 5;
+       int http_err = 0;
+       int ret = http_RecvMessage(
+               &info, &parser, HTTPMETHOD_GET, &timeout, &http_err);
+
+       httpmsg_destroy(&parser.msg);
+
+       EXPECT_EQ(ret, UPNP_E_OUTOF_BOUNDS);
+       EXPECT_EQ(http_err, HTTP_REQ_HEADER_FIELDS_TOO_LARGE);
+}
+
+// A header block under the limit must still be accepted.
+TEST_F(GhsaF86hTestSuite, AcceptsHeaderBlockUnderLimit)
+{
+       static const char req[] = "GET /desc.xml HTTP/1.1\r\n"
+                                 "HOST: 127.0.0.1:49152\r\n"
+                                 "USER-AGENT: test/1.0\r\n"
+                                 "\r\n";
+
+       write(sv[1], req, sizeof(req) - 1);
+       close(sv[1]);
+       sv[1] = -1;
+
+       SOCKINFO info{};
+       info.socket = sv[0];
+
+       http_parser_t parser{};
+       int timeout = 5;
+       int http_err = 0;
+       int ret = http_RecvMessage(
+               &info, &parser, HTTPMETHOD_UNKNOWN, &timeout, &http_err);
+
+       httpmsg_destroy(&parser.msg);
+
+       EXPECT_EQ(ret, UPNP_E_SUCCESS);
+}
+
+// The default limit must be generous enough for real UPnP traffic: a SUBSCRIBE
+// with a long multi-URL CALLBACK header is the worst realistic case.
+TEST_F(GhsaF86hTestSuite, AcceptsTypicalUpnpRequestAtDefaultLimit)
+{
+       g_maxHeaderSize = DEFAULT_MAX_HEADER_SIZE;
+
+       std::string req = "SUBSCRIBE /upnp/event/service HTTP/1.1\r\n"
+                         "HOST: 192.168.1.1:49152\r\n"
+                         "USER-AGENT: OS/1.0 UPnP/2.0 product/1.0\r\n"
+                         "CALLBACK: ";
+       for (int i = 0; i < 20; i++)
+               req += "<http://192.168.1.100:8080/event/callback/"; +
+                      std::to_string(i) + "> ";
+       req += "\r\n"
+              "NT: upnp:event\r\n"
+              "TIMEOUT: Second-1800\r\n"
+              "\r\n";
+       ASSERT_LT(req.size(), g_maxHeaderSize);
+
+       write(sv[1], req.data(), req.size());
+       close(sv[1]);
+       sv[1] = -1;
+
+       SOCKINFO info{};
+       info.socket = sv[0];
+
+       http_parser_t parser{};
+       int timeout = 5;
+       int http_err = 0;
+       int ret = http_RecvMessage(
+               &info, &parser, HTTPMETHOD_UNKNOWN, &timeout, &http_err);
+
+       httpmsg_destroy(&parser.msg);
+
+       EXPECT_EQ(ret, UPNP_E_SUCCESS);
+}
+
+// The header limit must not leak into the entity. A chunked body larger than
+// g_maxHeaderSize is entity data, not header data, and is governed by
+// g_maxContentLength — the check is gated on parser position precisely so that
+// chunked trailer headers, parsed at POS_ENTITY, are not caught by it.
+TEST_F(GhsaF86hTestSuite, DoesNotApplyHeaderLimitToEntity)
+{
+       g_maxContentLength = 65536; /* well above the body below */
+
+       static const char hdr[] = "HTTP/1.1 200 OK\r\n"
+                                 "Transfer-Encoding: chunked\r\n"
+                                 "\r\n";
+       char chunk_data[4096]; /* 4x the 1 KB header limit */
+       memset(chunk_data, 'X', sizeof(chunk_data));
+
+       write(sv[1], hdr, sizeof(hdr) - 1);
+       write(sv[1], "1000\r\n", 6); /* 0x1000 = 4096 bytes */
+       write(sv[1], chunk_data, sizeof(chunk_data));
+       write(sv[1], "\r\n", 2);
+       write(sv[1], "0\r\n\r\n", 5); /* terminator */
+       close(sv[1]);
+       sv[1] = -1;
+
+       SOCKINFO info{};
+       info.socket = sv[0];
+
+       http_parser_t parser{};
+       int timeout = 5;
+       int http_err = 0;
+       int ret = http_RecvMessage(
+               &info, &parser, HTTPMETHOD_GET, &timeout, &http_err);
+
+       httpmsg_destroy(&parser.msg);
+
+       EXPECT_EQ(ret, UPNP_E_SUCCESS);
+}
+
+// Setting the limit to 0 disables the check, matching UpnpSetMaxContentLength.
+TEST_F(GhsaF86hTestSuite, ZeroLimitDisablesTheCheck)
+{
+       g_maxHeaderSize = 0;
+
+       std::string req = "GET /desc.xml HTTP/1.1\r\n"
+                         "HOST: 127.0.0.1:49152\r\n";
+       for (int i = 0; i < 200; i++)
+               req += "X-Pad-" + std::to_string(i) + ": v\r\n";
+       req += "\r\n";
+
+       write(sv[1], req.data(), req.size());
+       close(sv[1]);
+       sv[1] = -1;
+
+       SOCKINFO info{};
+       info.socket = sv[0];
+
+       http_parser_t parser{};
+       int timeout = 5;
+       int http_err = 0;
+       int ret = http_RecvMessage(
+               &info, &parser, HTTPMETHOD_UNKNOWN, &timeout, &http_err);
+
+       httpmsg_destroy(&parser.msg);
+
+       EXPECT_EQ(ret, UPNP_E_SUCCESS);
+}
+
+// 431 must render a reason phrase: the 4xx table previously stopped at 417,
+// so reaching index 31 required extending it.
+TEST_F(GhsaF86hTestSuite, StatusCode431HasReasonPhrase)
+{
+       const char *text = http_get_code_text(HTTP_REQ_HEADER_FIELDS_TOO_LARGE);
+
+       ASSERT_NE(text, nullptr);
+       EXPECT_STREQ(text, "Request Header Fields Too Large");
+}
+
 int main(int argc, char **argv)
 {
        ::testing::InitGoogleTest(&argc, argv);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/libupnp.spec 
new/libupnp-22.1.0/libupnp.spec
--- old/libupnp-22.0.6/libupnp.spec     2026-08-12 21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/libupnp.spec     2026-09-10 22:47:00.000000000 +0200
@@ -1,4 +1,4 @@
-Version: 22.0.6
+Version: 22.1.0
 Summary: Universal Plug and Play (UPnP) SDK
 Name: libupnp-1.18
 Release: 1%{?dist}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/upnp/inc/upnp.h 
new/libupnp-22.1.0/upnp/inc/upnp.h
--- old/libupnp-22.0.6/upnp/inc/upnp.h  2026-08-12 21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/upnp/inc/upnp.h  2026-09-10 22:47:00.000000000 +0200
@@ -1088,6 +1088,11 @@
  * The default maximum content-length is \c DEFAULT_SOAP_CONTENT_LENGTH
  * = 64K bytes.
  *
+ * \note This limit applies to the entity only. The HTTP header block is
+ * bounded separately by \b UpnpSetMaxHeaderSize.
+ *
+ * \sa UpnpSetMaxHeaderSize
+ *
  * \return An integer representing one of the following:
  *     \li \c UPNP_E_SUCCESS: The operation completed successfully.
  */
@@ -1096,6 +1101,41 @@
         * actions, in bytes. */
        size_t contentLength);
 
+/*!
+ * \brief Sets the maximum size of the HTTP header block that the SDK will
+ * accept on an incoming request or response.
+ *
+ * The header block is the request or response line together with the header
+ * fields that follow it, up to the terminating empty line. Bounding it limits
+ * the memory a single peer can make the parser allocate before any request
+ * handler runs. Chunked trailer headers are part of the entity and are
+ * governed by \b UpnpSetMaxContentLength instead.
+ *
+ * This limit is independent of the content-length limit, so that raising the
+ * permissible entity size does not implicitly grant an equally large header
+ * allowance.
+ *
+ * A request whose header block exceeds this limit is rejected with
+ * HTTP 431 Request Header Fields Too Large.
+ *
+ * If set to 0 then checking will be disabled.
+ *
+ * The default maximum header size is \c DEFAULT_MAX_HEADER_SIZE
+ * = 16K bytes, which is ample for normal UPnP traffic. A peer that
+ * legitimately needs a larger header block is rejected until this is raised;
+ * the rejection is logged at \c UPNP_ERROR level and names this function.
+ *
+ * \sa UpnpSetMaxContentLength
+ *
+ * \return An integer representing one of the following:
+ *     \li \c UPNP_E_SUCCESS: The operation completed successfully.
+ *     \li \c UPNP_E_FINISH: The SDK is not initialized.
+ */
+UPNP_EXPORT_SPEC int UpnpSetMaxHeaderSize(
+       /*! [in] The maximum permissible size of the header block on incoming
+        * requests and responses, in bytes. */
+       size_t headerSize);
+
 /* @} Initialization and Registration */
 
 /******************************************************************************
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/upnp/src/api/upnpapi.c 
new/libupnp-22.1.0/upnp/src/api/upnpapi.c
--- old/libupnp-22.0.6/upnp/src/api/upnpapi.c   2026-08-12 21:42:59.000000000 
+0200
+++ new/libupnp-22.1.0/upnp/src/api/upnpapi.c   2026-09-10 22:47:00.000000000 
+0200
@@ -214,6 +214,7 @@
  * packet. Content-Length exceeding this size will be not processed and
  * error 413 (HTTP Error Code) will be returned to the remote end point. */
 size_t g_maxContentLength = DEFAULT_SOAP_CONTENT_LENGTH;
+size_t g_maxHeaderSize = DEFAULT_MAX_HEADER_SIZE;
 
 /*! Global variable to determines the maximum number of
  *  events which can be queued for a given subscription before events begin
@@ -4793,6 +4794,16 @@
 
        return UPNP_E_SUCCESS;
 }
+
+int UpnpSetMaxHeaderSize(size_t headerSize)
+{
+       if (UpnpSdkInit != 1) {
+               return UPNP_E_FINISH;
+       }
+       g_maxHeaderSize = headerSize;
+
+       return UPNP_E_SUCCESS;
+}
 
 int UpnpSetEventQueueLimits(int maxLen, int maxAge)
 {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/upnp/src/genlib/net/http/httpparser.c 
new/libupnp-22.1.0/upnp/src/genlib/net/http/httpparser.c
--- old/libupnp-22.0.6/upnp/src/genlib/net/http/httpparser.c    2026-08-12 
21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/upnp/src/genlib/net/http/httpparser.c    2026-09-10 
22:47:00.000000000 +0200
@@ -2143,6 +2143,41 @@
                parser->http_error_code = HTTP_INTERNAL_SERVER_ERROR;
                return PARSE_FAILURE;
        }
+       /* Bound the header block. While the request/response line and the
+        * header fields are still being read, everything buffered so far is
+        * header data, so the message buffer length is the header block size.
+        * The check must happen here rather than inside the header loop of
+        * parser_parse_headers(): a single unterminated header line keeps
+        * match() returning PARSE_INCOMPLETE, so the loop body never runs and
+        * a per-header counter would never advance while the line grows
+        * without bound.
+        *
+        * Chunked trailer headers are parsed with position == POS_ENTITY, and
+        * are excluded here because by then the buffer also holds the entity;
+        * they remain covered by g_maxContentLength. */
+       if (g_maxHeaderSize > 0 &&
+               (parser->position == (parser_pos_t)POS_REQUEST_LINE ||
+                       parser->position == (parser_pos_t)POS_RESPONSE_LINE ||
+                       parser->position == (parser_pos_t)POS_HEADERS) &&
+               parser->msg.msg.length > g_maxHeaderSize) {
+               /* Name the knob in the log: this rejection is new behaviour,
+                * so a peer that used to work can start failing on upgrade,
+                * and the caller only sees UPNP_E_OUTOF_BOUNDS. Sizes only,
+                * never the header bytes themselves, which are attacker
+                * controlled. */
+               UpnpPrintf(UPNP_ERROR,
+                       HTTP,
+                       __FILE__,
+                       __LINE__,
+                       "HTTP header block of %lu bytes exceeds the %lu byte "
+                       "limit; rejecting with %d. Raise it with "
+                       "UpnpSetMaxHeaderSize() if this peer is legitimate.\n",
+                       (unsigned long)parser->msg.msg.length,
+                       (unsigned long)g_maxHeaderSize,
+                       HTTP_REQ_HEADER_FIELDS_TOO_LARGE);
+               parser->http_error_code = HTTP_REQ_HEADER_FIELDS_TOO_LARGE;
+               return PARSE_FAILURE;
+       }
 
        return parser_parse(parser);
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/libupnp-22.0.6/upnp/src/genlib/net/http/httpreadwrite.c 
new/libupnp-22.1.0/upnp/src/genlib/net/http/httpreadwrite.c
--- old/libupnp-22.0.6/upnp/src/genlib/net/http/httpreadwrite.c 2026-08-12 
21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/upnp/src/genlib/net/http/httpreadwrite.c 2026-09-10 
22:47:00.000000000 +0200
@@ -438,7 +438,9 @@
                        case PARSE_NO_MATCH:
                                *http_error_code = parser->http_error_code;
                                if (*http_error_code ==
-                                       HTTP_REQ_ENTITY_TOO_LARGE) {
+                                               HTTP_REQ_ENTITY_TOO_LARGE ||
+                                       *http_error_code ==
+                                               
HTTP_REQ_HEADER_FIELDS_TOO_LARGE) {
                                        ret = UPNP_E_OUTOF_BOUNDS;
                                } else {
                                        ret = UPNP_E_BAD_HTTPMSG;
@@ -536,6 +538,28 @@
                        "(http_RecvMessage): Error %d, http_error_code = %d.\n",
                        ret,
                        *http_error_code);
+               /* Every g_maxContentLength rejection converges here, whether
+                * raised in this function or propagated from the parser, so
+                * the knob is named once instead of at each of the six sites.
+                * The caller only sees UPNP_E_OUTOF_BOUNDS, which does not say
+                * which limit was hit. Sizes only, never the body, which is
+                * attacker controlled. */
+               if (*http_error_code == HTTP_REQ_ENTITY_TOO_LARGE) {
+                       UpnpPrintf(UPNP_ERROR,
+                               HTTP,
+                               __FILE__,
+                               line,
+                               "HTTP entity exceeds the %lu byte limit "
+                               "(Content-Length %lu, chunk %lu, received "
+                               "%lu); rejecting with %d. Raise it with "
+                               "UpnpSetMaxContentLength() if this peer is "
+                               "legitimate.\n",
+                               (unsigned long)g_maxContentLength,
+                               (unsigned long)parser->content_length,
+                               (unsigned long)parser->chunk_size,
+                               (unsigned long)parser->msg.entity.length,
+                               HTTP_REQ_ENTITY_TOO_LARGE);
+               }
        }
 
        return ret;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/upnp/src/genlib/net/http/statcodes.c 
new/libupnp-22.1.0/upnp/src/genlib/net/http/statcodes.c
--- old/libupnp-22.0.6/upnp/src/genlib/net/http/statcodes.c     2026-08-12 
21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/upnp/src/genlib/net/http/statcodes.c     2026-09-10 
22:47:00.000000000 +0200
@@ -71,7 +71,7 @@
                                "\0"
                                "Temporary Redirect\0";
 
-#define NUM_4XX_CODES 18
+#define NUM_4XX_CODES 32
 static const char *Http4xxCodes[NUM_4XX_CODES];
 static const char *Http4xxStr = "Bad Request\0"
                                "Unauthorized\0"
@@ -90,7 +90,22 @@
                                "Request-URI Too Long\0"
                                "Unsupported Media Type\0"
                                "Requested Range Not Satisfiable\0"
-                               "Expectation Failed\0";
+                               "Expectation Failed\0"
+                               /* 418 - 430 are unused */
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "\0"
+                               "Request Header Fields Too Large\0";
 
 #define NUM_5XX_CODES 11
 static const char *Http5xxCodes[NUM_5XX_CODES];
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/upnp/src/inc/config.h 
new/libupnp-22.1.0/upnp/src/inc/config.h
--- old/libupnp-22.0.6/upnp/src/inc/config.h    2026-08-12 21:42:59.000000000 
+0200
+++ new/libupnp-22.1.0/upnp/src/inc/config.h    2026-09-10 22:47:00.000000000 
+0200
@@ -205,6 +205,23 @@
 /* @} */
 
 /*!
+ * \name DEFAULT_MAX_HEADER_SIZE
+ *
+ * The HTTP request or response line plus the header fields that follow it
+ * will read at most {\tt DEFAULT_MAX_HEADER_SIZE} bytes. This bounds the
+ * memory a single unauthenticated peer can make the parser allocate before
+ * any request handler runs, and it bounds the linear duplicate-name scan
+ * performed for every header that is added. It is deliberately separate
+ * from {\tt DEFAULT_SOAP_CONTENT_LENGTH}, so that raising the entity limit
+ * does not implicitly grant an equally large header allowance.
+ * This can be adjusted dynamically with {\tt UpnpSetMaxHeaderSize}.
+ *
+ * @{
+ */
+#define DEFAULT_MAX_HEADER_SIZE 16384
+/* @} */
+
+/*!
  * \name NUM_SSDP_COPY
  *
  * This configuration parameter determines how many copies of each SSDP
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/upnp/src/inc/statcodes.h 
new/libupnp-22.1.0/upnp/src/inc/statcodes.h
--- old/libupnp-22.0.6/upnp/src/inc/statcodes.h 2026-08-12 21:42:59.000000000 
+0200
+++ new/libupnp-22.1.0/upnp/src/inc/statcodes.h 2026-09-10 22:47:00.000000000 
+0200
@@ -72,6 +72,7 @@
 #define HTTP_UNSUPPORTED_MEDIA_TYPE 415
 #define HTTP_REQUEST_RANGE_NOT_SATISFIABLE 416
 #define HTTP_EXPECTATION_FAILED 417
+#define HTTP_REQ_HEADER_FIELDS_TOO_LARGE 431
 
 #define HTTP_INTERNAL_SERVER_ERROR 500
 #define HTTP_NOT_IMPLEMENTED 501
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/upnp/src/inc/upnpapi.h 
new/libupnp-22.1.0/upnp/src/inc/upnpapi.h
--- old/libupnp-22.0.6/upnp/src/inc/upnpapi.h   2026-08-12 21:42:59.000000000 
+0200
+++ new/libupnp-22.1.0/upnp/src/inc/upnpapi.h   2026-09-10 22:47:00.000000000 
+0200
@@ -52,9 +52,12 @@
 
 #define DEFAULT_SOAP_CONTENT_LENGTH 64000
 
+#define DEFAULT_MAX_HEADER_SIZE 16384
+
 #define NUM_HANDLE 200
 
 extern size_t g_maxContentLength;
+extern size_t g_maxHeaderSize;
 extern int g_UpnpSdkEQMaxLen;
 extern int g_UpnpSdkEQMaxAge;
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/upnp/test/poc_ghsa_f8r2_nullbyte.c 
new/libupnp-22.1.0/upnp/test/poc_ghsa_f8r2_nullbyte.c
--- old/libupnp-22.0.6/upnp/test/poc_ghsa_f8r2_nullbyte.c       2026-08-12 
21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/upnp/test/poc_ghsa_f8r2_nullbyte.c       2026-09-10 
22:47:00.000000000 +0200
@@ -46,8 +46,8 @@
 /* The miniserver rejects requests whose Host header isn't a numeric
  * ip:port (DNS-rebinding protection), so the Host header below must
  * match the server's own address. */
-static int get_status_code(const char *ip, unsigned short port,
-       const char *path)
+static int get_status_code(
+       const char *ip, unsigned short port, const char *path)
 {
        int sock;
        struct sockaddr_in addr;
@@ -82,8 +82,7 @@
        send(sock, req, strlen(req), MSG_NOSIGNAL);
 
        while (total < sizeof buf - 1 &&
-               (n = recv(sock, buf + total, sizeof buf - 1 - total, 0)) >
-                       0) {
+               (n = recv(sock, buf + total, sizeof buf - 1 - total, 0)) > 0) {
                total += (size_t)n;
        }
        buf[total] = '\0';
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.0.6/upnp/test/test_gena_subscribe_dos.c 
new/libupnp-22.1.0/upnp/test/test_gena_subscribe_dos.c
--- old/libupnp-22.0.6/upnp/test/test_gena_subscribe_dos.c      2026-08-12 
21:42:59.000000000 +0200
+++ new/libupnp-22.1.0/upnp/test/test_gena_subscribe_dos.c      2026-09-10 
22:47:00.000000000 +0200
@@ -24,13 +24,23 @@
  *
  * Test cases:
  *   A. Oversized Callback header:
- *      Before fix 1 → HTTP 200 (accepted, 65536-byte alloc) → FAIL
+ *      Before fix 1 → HTTP 200 (accepted, oversized alloc)  → FAIL
  *      After  fix 1 → HTTP 412 (rejected before alloc)      → PASS
  *
  *   B. Subscription count limit (MaxSubscriptions = SMALL_LIMIT):
  *      Before fix 2 → all N+1 requests return HTTP 200      → FAIL
  *      After  fix 2 → requests 1..N return 200, N+1 → 500  → PASS
  *
+ *   C. Oversized header block (GHSA-f86h-hm8r-cqxr):
+ *      Before      → HTTP 200 (whole block buffered)        → FAIL
+ *      After       → HTTP 431 (rejected on size)            → PASS
+ *
+ * Note that A and C are distinct layers. The Callback path in A is sized to
+ * exceed MAX_SUBSCRIPTION_CALLBACK_HEADER_SIZE while keeping the header block
+ * under DEFAULT_MAX_HEADER_SIZE, so the request still reaches GENA; C uses the
+ * reporter's original 65536-byte path, which the parser now rejects before
+ * GENA is ever consulted.
+ *
  * The Callback URL always uses the server's own IP so that
  * gena_validate_delivery_urls() passes the subnet check.
  */
@@ -54,8 +64,16 @@
 /* Path the registered service listens on for SUBSCRIBE. */
 #define EVENT_URL_PATH "/event/dos435"
 
-/* Callback header size that matches the reporter's PoC. */
-#define LARGE_CB_PATH_LEN 0x10000u /* 65536 bytes */
+/* Callback path long enough to exceed MAX_SUBSCRIPTION_CALLBACK_HEADER_SIZE
+ * (5000) while keeping the whole header block under DEFAULT_MAX_HEADER_SIZE
+ * (16384), so that this case still reaches the GENA layer and exercises fix 1
+ * rather than being rejected earlier by the parser's header-block limit. */
+#define LARGE_CB_PATH_LEN 8192u
+
+/* Callback path large enough to push the header block past
+ * DEFAULT_MAX_HEADER_SIZE, which the parser must reject on its own. This is
+ * the size the reporter's original PoC used. */
+#define OVERSIZED_HDR_CB_PATH_LEN 0x10000u /* 65536 bytes */
 
 /* How many subscriptions to accept before capping in test B. */
 #define SMALL_LIMIT 3
@@ -209,6 +227,49 @@
 }
 
 /*
+ * Test C (GHSA-f86h-hm8r-cqxr): a header block larger than
+ * DEFAULT_MAX_HEADER_SIZE must be rejected by the parser with HTTP 431,
+ * before the request is dispatched to GENA at all. Driving this over a real
+ * socket also confirms that 431 reaches the wire with its reason phrase,
+ * which required extending the 4xx status table past 417.
+ */
+static int test_oversized_header_block(
+       const char *server_ip, unsigned short server_port)
+{
+       char *large_path;
+       int status;
+
+       large_path = malloc(OVERSIZED_HDR_CB_PATH_LEN);
+       if (!large_path) {
+               perror("malloc");
+               return -1;
+       }
+       memset(large_path, 'a', OVERSIZED_HDR_CB_PATH_LEN);
+
+       status = send_subscribe(
+               server_ip, server_port, large_path, OVERSIZED_HDR_CB_PATH_LEN);
+       free(large_path);
+
+       printf("Test C: SUBSCRIBE with %u-byte header block -> HTTP %d\n",
+               OVERSIZED_HDR_CB_PATH_LEN,
+               status);
+
+       if (status == 431) {
+               puts("Test C PASS: oversized header block rejected (HTTP "
+                    "431).");
+               return 0;
+       }
+       fprintf(stderr,
+               "Test C FAIL: expected HTTP 431, got %d.\n"
+               "  Anything else means the %u-byte header block was buffered "
+               "instead of\n"
+               "  being rejected on size (GHSA-f86h-hm8r-cqxr).\n",
+               status,
+               OVERSIZED_HDR_CB_PATH_LEN);
+       return -1;
+}
+
+/*
  * Test B: After MaxSubscriptions subscriptions are registered the next
  * SUBSCRIBE must be rejected with HTTP 500.
  * Uses UpnpSetMaxSubscriptions() to force a small limit so the test runs fast.
@@ -315,6 +376,9 @@
        if (test_oversized_callback(server_ip, server_port) != 0)
                result = 1;
 
+       if (test_oversized_header_block(server_ip, server_port) != 0)
+               result = 1;
+
        if (test_subscription_count_limit(handle, server_ip, server_port) != 0)
                result = 1;
 

Reply via email to