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;
