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-08-15 22:40:11 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libupnp (Old) and /work/SRC/openSUSE:Factory/.libupnp.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libupnp" Sat Aug 15 22:40:11 2026 rev:49 rq:1370965 version:22.0.6 Changes: -------- --- /work/SRC/openSUSE:Factory/libupnp/libupnp.changes 2026-07-20 09:57:05.695597178 +0200 +++ /work/SRC/openSUSE:Factory/.libupnp.new.1258/libupnp.changes 2026-08-15 22:40:18.554917101 +0200 @@ -1,0 +2,6 @@ +Thu Aug 13 07:42:55 UTC 2026 - Jan Engelhardt <[email protected]> + +- Update to release 22.0.6 + * Build fixes for OmniOS + +------------------------------------------------------------------- Old: ---- libupnp-22.0.4.tar.bz2 New: ---- libupnp-22.0.6.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libupnp.spec ++++++ --- /var/tmp/diff_new_pack.YioNRS/_old 2026-08-15 22:40:19.501950437 +0200 +++ /var/tmp/diff_new_pack.YioNRS/_new 2026-08-15 22:40:19.503950507 +0200 @@ -19,7 +19,7 @@ %define pnpver 22 Name: libupnp -Version: 22.0.4 +Version: 22.0.6 Release: 0 Summary: An implementation of Universal Plug and Play (UPnP) License: BSD-3-Clause ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.YioNRS/_old 2026-08-15 22:40:19.544951951 +0200 +++ /var/tmp/diff_new_pack.YioNRS/_new 2026-08-15 22:40:19.547952056 +0200 @@ -1,5 +1,5 @@ -mtime: 1784230268 -commit: 6c3a42da6f231a216489acbd991b7f515ddf845ea1b81dd1db1794d5bd5990b6 +mtime: 1786608334 +commit: 333233cbdac79e094a1767518106d4d755f3b5c2837b893456137867b1858393 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-08-13 10:05:34.000000000 +0200 @@ -0,0 +1 @@ +.osc ++++++ libupnp-22.0.4.tar.bz2 -> libupnp-22.0.6.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/CMakeLists.txt new/libupnp-22.0.6/CMakeLists.txt --- old/libupnp-22.0.4/CMakeLists.txt 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/CMakeLists.txt 2026-08-12 21:42:59.000000000 +0200 @@ -7,7 +7,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) project(PUPNP - VERSION 22.0.4 + VERSION 22.0.6 LANGUAGES C) include(GNUInstallDirs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/ChangeLog new/libupnp-22.0.6/ChangeLog --- old/libupnp-22.0.4/ChangeLog 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/ChangeLog 2026-08-12 21:42:59.000000000 +0200 @@ -1,4 +1,37 @@ ******************************************************************************* +Version 22.0.6 +******************************************************************************* + +- Fix OmniOS build failure in test-upnp-ghsa-f8r2. The GHSA-279g-pwr2-f8r2 + regression test calls socket()/connect()/send()/recv() directly, same as + test-upnp-gh-325, but was registered without the SOCKET_LIBRARY/ + NSL_LIBRARY linkage those symbols need outside glibc (e.g. Solaris/ + illumos). Also fixes a stray placement bug where its registration had + landed between gh-325's registration and gh-325's own linking block. + +******************************************************************************* +Version 22.0.5 +******************************************************************************* + +- GHSA-279g-pwr2-f8r2: Fix path truncation via embedded NUL byte (%00) in + request paths. remove_escaped_chars() correctly decodes %00 to a literal + NUL byte, but process_request() then treated the decoded buffer as a + NUL-terminated C string in alias/virtual-directory routing checks, so a + request for /alias%00.png was silently served as /alias, bypassing + extension-based ACL/WAF filters and virtual-directory routing. Fixed by + rejecting the request when the decoded string length no longer matches + the tracked buffer length. +- GHSA-hg5x-73vf-vfm2: Fix unbounded memory allocation and integer + truncation in chunked Transfer-Encoding parsing. match_int() parsed a hex + chunk size into a 64-bit long but only rejected negative values, so a + declared size above INT_MAX silently wrapped (e.g. 2^32 wrapped to 0 and + was misread as the chunked-entity terminator). Separately, a declared + chunk size was never checked against g_maxContentLength before its bytes + were buffered, allowing a single oversized chunk to exhaust memory. + Fixed by rejecting out-of-range chunk sizes and rejecting a chunk size + exceeding g_maxContentLength before any of its body is read. + +******************************************************************************* Version 22.0.4 ******************************************************************************* diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/README.md new/libupnp-22.0.6/README.md --- old/libupnp-22.0.4/README.md 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/README.md 2026-08-12 21:42:59.000000000 +0200 @@ -5,7 +5,8 @@ | branch | status | | -------------- | --------------------------------------------------------------------------------------------------- | -| main (2.0.x) |  | +| main (22.0.x) |  | +| branch-2.0.x |  | | branch-1.18.x |  | | branch-1.14.x |  | | branch-1.12.x |  | @@ -116,7 +117,12 @@ | Release Number | Date | History | | -------------- | ---------- | ---------------------------------------- | +| 22.0.5 | 2026-08-12 | [Portable UPnP SDK][Portable UPnP SDK] | +| 22.0.4 | 2026-07-15 | [Portable UPnP SDK][Portable UPnP SDK] | | 22.0.3 | 2026-07-14 | [Portable UPnP SDK][Portable UPnP SDK] | +| 22.0.2 | NA | Internal | +| 22.0.1 | NA | Internal | +| 22.0.0 | NA | Internal | | 2.0.2 | 2026-06-17 | [Portable UPnP SDK][Portable UPnP SDK] | | 2.0.1 | 2026-06-17 | [Portable UPnP SDK][Portable UPnP SDK] | | 2.0.0 | 2026-06-16 | [Portable UPnP SDK][Portable UPnP SDK] | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/SECURITY.md new/libupnp-22.0.6/SECURITY.md --- old/libupnp-22.0.4/SECURITY.md 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/SECURITY.md 2026-08-12 21:42:59.000000000 +0200 @@ -6,9 +6,8 @@ | Version | Supported | | -------- | ------------------ | -| 1.18.x | :white_check_mark: | -| 1.14.x | :x: | -| < 1.14.x | :x: | +| 22.0.x | :white_check_mark: | +| < 22.0.x | :x: | ## Reporting a Vulnerability @@ -30,3 +29,12 @@ Nonetheless, we will try to provide a fix for the problem as soon as possible and request that you do not disclose the issue publicly until we have released a patch. + +If you would like to contribute the fix yourself, we do not accept public pull +requests for an unpatched vulnerability, since that would expose the issue +before a fix is available to users. Instead, we can set up a temporary private +fork attached to the advisory so you can collaborate with us and submit your +patch there directly. + +Once a fix is released, we publish the advisory and request a CVE ID through +GitHub. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/docs/Doxyfile new/libupnp-22.0.6/docs/Doxyfile --- old/libupnp-22.0.4/docs/Doxyfile 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/docs/Doxyfile 2026-08-12 21:42:59.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.4 +PROJECT_NUMBER = 22.0.6 # 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.4/gtest/test_httpreadwrite.cpp new/libupnp-22.0.6/gtest/test_httpreadwrite.cpp --- old/libupnp-22.0.4/gtest/test_httpreadwrite.cpp 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/gtest/test_httpreadwrite.cpp 2026-08-12 21:42:59.000000000 +0200 @@ -297,6 +297,153 @@ EXPECT_EQ(ret, UPNP_E_SUCCESS); } +// regression: GHSA-hg5x-73vf-vfm2 +// Two related bugs in chunked Transfer-Encoding parsing: +// +// 1. A hex chunk-size token is parsed by match_int() into a 64-bit `long` +// (via strtol) but only checked for `num < 0`, then narrowed to `int` +// with `*value = (int)num;` — silently wrapping values above INT_MAX +// instead of rejecting them. Worse, the call site +// (parser_parse_chunky_entity, "%x%L%c") passes `&parser->chunk_size`, +// which is `size_t`, into a vararg slot read back as `int *` +// (httpparser.c's match(), case 'x'/'d') — a type-confused write that +// only ever touches the low 4 bytes of the 8-byte field. A declared +// chunk size of "100000000" (2^32) wraps to 0 and is misread as the +// chunked-entity terminator, letting an attacker-declared multi- +// gigabyte chunk be silently accepted as an empty, already-complete +// entity. +// +// 2. Even with a syntactically valid (in-range) chunk size, the declared +// size is never compared against g_maxContentLength before the parser +// buffers the chunk's bytes: parser->msg.entity.length is only +// incremented in parser_parse_chunky_body() once the *entire* declared +// chunk has already been received into parser->msg.msg (see +// httpparser.c:1693), so the existing g_maxContentLength check in +// http_RecvMessage() (which reads entity.length) never fires while a +// single oversized chunk is still streaming in. +class GhsaHg5xTestSuite : public ::testing::Test +{ +protected: + int sv[2]{-1, -1}; + size_t saved_limit_{}; + + void SetUp() override + { + saved_limit_ = g_maxContentLength; + g_maxContentLength = 1024; /* 1 KB limit */ + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0); + } + + void TearDown() override + { + g_maxContentLength = saved_limit_; + if (sv[0] >= 0) + close(sv[0]); + if (sv[1] >= 0) + close(sv[1]); + } +}; + +// A chunk-size line declaring 2^32 ("100000000") must not be silently +// truncated to 0 and treated as an already-complete, empty entity. +TEST_F(GhsaHg5xTestSuite, OverflowingChunkSizeIsRejected) +{ + static const char hdr[] = "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"; + + write(sv[1], hdr, sizeof(hdr) - 1); + /* 0x100000000 = 2^32; (int) truncation wraps this to 0, which is + * the chunked-entity terminator, followed by an empty trailer + * header block. */ + write(sv[1], "100000000\r\n", 11); + write(sv[1], "\r\n", 2); + 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_NE(ret, UPNP_E_SUCCESS); +} + +// A single declared chunk larger than g_maxContentLength must be rejected +// as soon as its buffered bytes exceed the limit, without waiting for the +// full declared chunk size to be received. +TEST_F(GhsaHg5xTestSuite, OversizedSingleChunkIsRejectedBeforeBuffering) +{ + static const char hdr[] = "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"; + + char chunk_data[2048]; + memset(chunk_data, 'X', sizeof(chunk_data)); + + write(sv[1], hdr, sizeof(hdr) - 1); + /* Declare an 8 KB chunk (0x2000), but only ever send 2 KB of it — + * more than g_maxContentLength (1 KB), but less than the declared + * chunk size, so the chunk never completes and entity.length is + * never updated. */ + write(sv[1], "2000\r\n", 6); + write(sv[1], chunk_data, sizeof(chunk_data)); + 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_ENTITY_TOO_LARGE); +} + +// A chunk size within range and under the limit must still be accepted. +TEST_F(GhsaHg5xTestSuite, ValidChunkSizeIsAccepted) +{ + static const char hdr[] = "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"; + + char chunk_data[512]; + memset(chunk_data, 'X', sizeof(chunk_data)); + + write(sv[1], hdr, sizeof(hdr) - 1); + write(sv[1], "200\r\n", 5); /* 0x200 = 512 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); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/libupnp.spec new/libupnp-22.0.6/libupnp.spec --- old/libupnp-22.0.4/libupnp.spec 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/libupnp.spec 2026-08-12 21:42:59.000000000 +0200 @@ -1,4 +1,4 @@ -Version: 22.0.4 +Version: 22.0.6 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.4/upnp/src/genlib/net/http/httpparser.c new/libupnp-22.0.6/upnp/src/genlib/net/http/httpparser.c --- old/libupnp-22.0.4/upnp/src/genlib/net/http/httpparser.c 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/upnp/src/genlib/net/http/httpparser.c 2026-08-12 21:42:59.000000000 +0200 @@ -43,6 +43,7 @@ #include "UpnpGlobal.h" #include "membuffer.h" #include "upnp.h" +#include "upnpapi.h" #include "upnpdebug.h" #include "uri.h" #include <errno.h> @@ -792,7 +793,8 @@ num = strtol(token.buf, &end_ptr, base); /* all and only those chars in token should be used for * num */ - if (num < 0 || end_ptr != token.buf + token.length || + if (num < 0 || num > INT_MAX || + end_ptr != token.buf + token.length || ((num == LONG_MIN || num == LONG_MAX) && (errno == ERANGE))) { status = PARSE_NO_MATCH; @@ -1691,13 +1693,21 @@ parser->scanner.cursor = save_pos; /*update temp */ parser->msg.entity.length += parser->chunk_size; + + /* Check content length limit during chunked accumulation */ + if (g_maxContentLength > 0 && + parser->msg.entity.length > + (unsigned int)g_maxContentLength) { + parser->http_error_code = HTTP_REQ_ENTITY_TOO_LARGE; + return PARSE_FAILURE; + } + parser->ent_position = ENTREAD_USING_CHUNKED; return PARSE_CONTINUE_1; } else /* need more data for chunk */ return PARSE_INCOMPLETE; } - /************************************************************************ * Function: parser_parse_chunky_headers * @@ -1760,13 +1770,14 @@ parse_status_t status; size_t save_pos; memptr dummy; + int chunk_size; assert(parser->ent_position == ENTREAD_USING_CHUNKED); save_pos = scanner->cursor; /* get size of chunk, discard extension, discard CRLF */ - status = match(scanner, "%x%L%c", &parser->chunk_size, &dummy); + status = match(scanner, "%x%L%c", &chunk_size, &dummy); if (status != (parse_status_t)PARSE_OK) { scanner->cursor = save_pos; UpnpPrintf(UPNP_INFO, @@ -1776,6 +1787,9 @@ "CHUNK COULD NOT BE PARSED\n"); return status; } + + parser->chunk_size = (size_t)chunk_size; + /* remove chunk info just matched; just retain data */ membuffer_delete( &parser->msg.msg, save_pos, (scanner->cursor - save_pos)); @@ -1790,13 +1804,18 @@ /* read entity headers */ parser->ent_position = ENTREAD_CHUNKY_HEADERS; } else { + /* Check if this single chunk exceeds the max content length */ + if (g_maxContentLength > 0 && + parser->chunk_size > (unsigned int)g_maxContentLength) { + parser->http_error_code = HTTP_REQ_ENTITY_TOO_LARGE; + return PARSE_FAILURE; + } /* read chunk body */ parser->ent_position = ENTREAD_CHUNKY_BODY; } return PARSE_CONTINUE_1; /* continue to reading body */ } - /************************************************************************ * Function: parser_parse_entity_until_close * diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/upnp/src/genlib/net/http/httpreadwrite.c new/libupnp-22.0.6/upnp/src/genlib/net/http/httpreadwrite.c --- old/libupnp-22.0.4/upnp/src/genlib/net/http/httpreadwrite.c 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/upnp/src/genlib/net/http/httpreadwrite.c 2026-08-12 21:42:59.000000000 +0200 @@ -437,15 +437,44 @@ case PARSE_FAILURE: case PARSE_NO_MATCH: *http_error_code = parser->http_error_code; + if (*http_error_code == + HTTP_REQ_ENTITY_TOO_LARGE) { + ret = UPNP_E_OUTOF_BOUNDS; + } else { + ret = UPNP_E_BAD_HTTPMSG; + } line = __LINE__; - ret = UPNP_E_BAD_HTTPMSG; goto ExitFunction; case PARSE_INCOMPLETE_ENTITY: /* read until close */ ok_on_close = 1; + /* Check content length limit during chunked + * entity accumulation */ + if (g_maxContentLength > 0 && + parser->msg.entity.length > + (unsigned int) + g_maxContentLength) { + *http_error_code = + HTTP_REQ_ENTITY_TOO_LARGE; + line = __LINE__; + ret = UPNP_E_OUTOF_BOUNDS; + goto ExitFunction; + } break; case PARSE_CONTINUE_1: /* Web post request. */ + /* Check content length limit for chunked/web + * post */ + if (g_maxContentLength > 0 && + parser->msg.entity.length > + (unsigned int) + g_maxContentLength) { + *http_error_code = + HTTP_REQ_ENTITY_TOO_LARGE; + line = __LINE__; + ret = UPNP_E_OUTOF_BOUNDS; + goto ExitFunction; + } line = __LINE__; ret = PARSE_SUCCESS; goto ExitFunction; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/upnp/src/genlib/net/http/webserver.c new/libupnp-22.0.6/upnp/src/genlib/net/http/webserver.c --- old/libupnp-22.0.4/upnp/src/genlib/net/http/webserver.c 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/upnp/src/genlib/net/http/webserver.c 2026-08-12 21:42:59.000000000 +0200 @@ -1284,6 +1284,14 @@ request_doc[url->pathquery.size] = '\0'; dummy = url->pathquery.size; remove_escaped_chars(request_doc, &dummy); + /* Prevent path truncation (NUL byte injection): + * If remove_escaped_chars decoded a %00, strlen will stop early, + * making it strictly less than the actual decoded length (dummy). */ + if (strlen(request_doc) != dummy) { + err_code = HTTP_BAD_REQUEST; + goto error_handler; + } + code = remove_dots(request_doc, url->pathquery.size); if (code != 0) { err_code = HTTP_FORBIDDEN; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/upnp/test/CMakeLists.txt new/libupnp-22.0.6/upnp/test/CMakeLists.txt --- old/libupnp-22.0.4/upnp/test/CMakeLists.txt 2026-07-15 03:45:23.000000000 +0200 +++ new/libupnp-22.0.6/upnp/test/CMakeLists.txt 2026-08-12 21:42:59.000000000 +0200 @@ -286,6 +286,25 @@ ) endif() +# GHSA-279g-pwr2-f8r2: embedded NUL byte (%00) in a request path is decoded +# to a literal NUL by remove_escaped_chars(), but downstream alias/routing +# checks in webserver.c use NUL-terminated string functions, so bytes after +# the NUL are silently dropped and the request wrongly matches a shorter +# registered alias. Uses the same web_server_ut_set_alias hook. +UPNP_Add_Unit_Test(test-upnp-ghsa-f8r2 poc_ghsa_f8r2_nullbyte.c) +if(UPNP_BUILD_SHARED) + target_link_libraries(test-upnp-ghsa-f8r2 PRIVATE + $<$<BOOL:${SOCKET_LIBRARY}>:${SOCKET_LIBRARY}> + $<$<BOOL:${NSL_LIBRARY}>:${NSL_LIBRARY}> + ) +endif() +if(UPNP_BUILD_STATIC) + target_link_libraries(test-upnp-ghsa-f8r2-static PRIVATE + $<$<BOOL:${SOCKET_LIBRARY}>:${SOCKET_LIBRARY}> + $<$<BOOL:${NSL_LIBRARY}>:${NSL_LIBRARY}> + ) +endif() + # Issue #347: HTTP response headers must use title-case names. # Starts the UPnP HTTP server via UpnpInit2, sends a GET request over a raw # TCP socket, and verifies title-case header names in the response. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.0.4/upnp/test/poc_ghsa_f8r2_nullbyte.c new/libupnp-22.0.6/upnp/test/poc_ghsa_f8r2_nullbyte.c --- old/libupnp-22.0.4/upnp/test/poc_ghsa_f8r2_nullbyte.c 1970-01-01 01:00:00.000000000 +0100 +++ new/libupnp-22.0.6/upnp/test/poc_ghsa_f8r2_nullbyte.c 2026-08-12 21:42:59.000000000 +0200 @@ -0,0 +1,169 @@ +/* poc_ghsa_f8r2_nullbyte.c + * + * Regression test for GHSA-279g-pwr2-f8r2: embedded NUL byte (%00) + * injection in HTTP request paths bypasses path handling. + * + * process_request() in webserver.c percent-decodes the request path with + * remove_escaped_chars(), which correctly turns "%00" into a literal NUL + * byte and tracks the true (post-decode) length in a separate size_t. + * Everything downstream, however, treats the decoded buffer as a plain + * C string (strcmp/strncmp/strlen in remove_dots(), isFileInVirtualDir(), + * get_alias(), membuffer_assign_str()), so any bytes after the embedded + * NUL are silently invisible to those checks. + * + * This lets a request for "/desc.xml%00.png" match an alias registered + * as "/desc.xml", even though the two are different byte sequences on + * the wire -- exactly the kind of internal/external request-path mismatch + * a WAF or reverse-proxy extension filter would not expect. + * + * Pre-fix: server returns 200 (silently matches the truncated alias). + * Post-fix: server rejects the request instead of matching the alias. + * + * regression: GHSA-279g-pwr2-f8r2 + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#ifndef _WIN32 + + #include "upnp.h" + + #include <arpa/inet.h> + #include <netinet/in.h> + #include <sys/socket.h> + #include <unistd.h> + +/* regression: GHSA-279g-pwr2-f8r2 -- test hook exported from libupnp */ +extern int web_server_ut_set_alias( + const char *name, const char *content, size_t len); + + #ifndef MSG_NOSIGNAL + #define MSG_NOSIGNAL 0 + #endif + +/* 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) +{ + int sock; + struct sockaddr_in addr; + char req[256]; + char buf[512]; + ssize_t n; + size_t total = 0; + int status = -1; + + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) + return -1; + + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + inet_pton(AF_INET, ip, &addr.sin_addr); + + if (connect(sock, (struct sockaddr *)&addr, sizeof addr) != 0) { + close(sock); + return -1; + } + + snprintf(req, + sizeof req, + "GET %s HTTP/1.1\r\n" + "Host: %s:%u\r\n" + "Connection: close\r\n\r\n", + path, + ip, + port); + send(sock, req, strlen(req), MSG_NOSIGNAL); + + while (total < sizeof buf - 1 && + (n = recv(sock, buf + total, sizeof buf - 1 - total, 0)) > + 0) { + total += (size_t)n; + } + buf[total] = '\0'; + close(sock); + + if (total > 0) { + sscanf(buf, "HTTP/%*d.%*d %d", &status); + } + + return status; +} + +int main(void) +{ + int rc; + char *ip; + unsigned short port; + int status; + + rc = UpnpInit2(NULL, 0); + if (rc != UPNP_E_SUCCESS) { + fprintf(stderr, + "UpnpInit2 failed (%d); skipping (no network?)\n", + rc); + return EXIT_SUCCESS; + } + + rc = web_server_ut_set_alias("/desc.xml", "<root/>", 7); + if (rc != UPNP_E_SUCCESS) { + fprintf(stderr, "web_server_ut_set_alias failed (%d)\n", rc); + UpnpFinish(); + return EXIT_FAILURE; + } + + ip = UpnpGetServerIpAddress(); + port = UpnpGetServerPort(); + if (ip == NULL || port == 0) { + fprintf(stderr, + "UpnpGetServerIpAddress/UpnpGetServerPort returned " + "nothing; skipping\n"); + UpnpFinish(); + return EXIT_SUCCESS; + } + + /* Sanity check: the plain alias must resolve, otherwise the '%00' + * check below would be meaningless. */ + if (get_status_code(ip, port, "/desc.xml") != 200) { + fprintf(stderr, + "Baseline request for '/desc.xml' did not return " + "200; skipping (environment issue?)\n"); + UpnpFinish(); + return EXIT_SUCCESS; + } + + /* "%00" decodes to a NUL byte; the registered alias is "/desc.xml", + * not "/desc.xml\0.png", so this must NOT be served as the alias. */ + status = get_status_code(ip, port, "/desc.xml%00.png"); + + UpnpFinish(); + + if (status == 200) { + fprintf(stderr, + "FAIL: server returned 200 for '/desc.xml%%00.png' " + "-- embedded NUL byte truncated the path to match " + "the '/desc.xml' alias (GHSA-279g-pwr2-f8r2)\n"); + return EXIT_FAILURE; + } + + printf("PASS: request with embedded NUL byte was not silently " + "truncated to match the alias (status=%d)\n", + status); + return EXIT_SUCCESS; +} + +#else /* _WIN32 */ + +int main(void) +{ + puts("SKIP: test uses POSIX sockets (not available on Windows)."); + return EXIT_SUCCESS; +} + +#endif /* _WIN32 */
