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-11-06 19:33:55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libupnp (Old) and /work/SRC/openSUSE:Factory/.libupnp.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libupnp" Fri Nov 6 19:33:55 2026 rev:56 rq:1395070 version:22.2.0 Changes: -------- --- /work/SRC/openSUSE:Factory/libupnp/libupnp.changes 2026-11-01 22:22:29.906709766 +0100 +++ /work/SRC/openSUSE:Factory/.libupnp.new.3759290/libupnp.changes 2026-11-06 19:37:01.688575355 +0100 @@ -1,0 +2,6 @@ +Wed Nov 4 17:15:36 UTC 2026 - Jan Engelhardt <[email protected]> + +- Update to release 22.2 + * Cast input to ctype calls to unsigned char + +------------------------------------------------------------------- Old: ---- libupnp-22.1.10.tar.bz2 libupnp-22.1.8.tar.bz2 New: ---- libupnp-22.2.0.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libupnp.spec ++++++ --- /var/tmp/diff_new_pack.vPx9Cd/_old 2026-11-06 19:37:02.883625343 +0100 +++ /var/tmp/diff_new_pack.vPx9Cd/_new 2026-11-06 19:37:02.885625427 +0100 @@ -19,7 +19,7 @@ %define pnpver 22 Name: libupnp -Version: 22.1.10 +Version: 22.2.0 Release: 0 Summary: An implementation of Universal Plug and Play (UPnP) License: BSD-3-Clause ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.vPx9Cd/_old 2026-11-06 19:37:02.938627644 +0100 +++ /var/tmp/diff_new_pack.vPx9Cd/_new 2026-11-06 19:37:02.944627895 +0100 @@ -1,5 +1,5 @@ -mtime: 1793319610 -commit: fea2f899ca7120ff7016375ed47b89ee6539649b5b1510dce9c601405e3da356 +mtime: 1793812723 +commit: c6f10f7be6bdf52b7f3911ea4ddd51196dc6ac0cb6da7d81cbbcd0db80b789fe 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-11-04 18:18:43.000000000 +0100 @@ -0,0 +1 @@ +.osc ++++++ libupnp-22.1.10.tar.bz2 -> libupnp-22.2.0.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/CMakeLists.txt new/libupnp-22.2.0/CMakeLists.txt --- old/libupnp-22.1.10/CMakeLists.txt 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/CMakeLists.txt 2026-11-04 14:48:44.000000000 +0100 @@ -7,7 +7,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) project(PUPNP - VERSION 22.1.10 + VERSION 22.2.0 LANGUAGES C) include(GNUInstallDirs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/ChangeLog new/libupnp-22.2.0/ChangeLog --- old/libupnp-22.1.10/ChangeLog 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/ChangeLog 2026-11-04 14:48:44.000000000 +0100 @@ -1,4 +1,33 @@ ******************************************************************************* +Version 22.2.0 +******************************************************************************* + +- GHSA-8rmx-x2cq-xh33: Authenticate the server of HTTPS connections. With + UPNP_ENABLE_OPEN_SSL, the client accepted any certificate and never checked + the host name, so an on-path attacker or a malicious device could intercept + an https:// connection. UpnpInitSslContext() now sets SSL_VERIFY_PEER with + the system's default trust store, and every connection checks that the + certificate matches the host of the URL (DNS name with SNI, or IP address). + SOAP actions, GENA subscriptions and NOTIFY, UpnpDownloadUrlItem() and + UpnpDownloadXmlDoc() have no TLS and sent https:// requests in clear; they + now return UPNP_E_INVALID_URL. + NOTE: this changes the default behaviour. A server with a self-signed or + otherwise untrusted certificate is now rejected; add its certificate or CA + to the context returned by UpnpGetSslCtx(), e.g. with + SSL_CTX_load_verify_locations(). pupnp honours the verify mode of the + context: SSL_VERIFY_NONE turns server authentication off, and a context + passed to UpnpSetSslCtx() must enable SSL_VERIFY_PEER and load a trust + store itself. + +- PR #666: Cast to unsigned char before calling the <ctype.h> functions in + the URI and HTTP parsers. parse_uri(), the HTTP header matching, the Range + parser and the pre-Vista inet_pton6() passed a plain char, so where char is + signed a byte from 0x80 to 0xFE became a negative int, which is undefined + behaviour (C11 7.4). These bytes come from the network: the request line, + SSDP LOCATION, description documents and GENA Callback headers. + + +******************************************************************************* Version 22.1.10 ******************************************************************************* diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/README.md new/libupnp-22.2.0/README.md --- old/libupnp-22.1.10/README.md 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/README.md 2026-11-04 14:48:44.000000000 +0100 @@ -108,6 +108,7 @@ | Release Number | Date | History | | -------------- | ---------- | ---------------------------------------- | +| 22.1.10 | 2026-10-29 | [Portable UPnP SDK][Portable UPnP SDK] | | 22.1.9 | 2026-10-28 | [Portable UPnP SDK][Portable UPnP SDK] | | 22.1.8 | 2026-09-30 | [Portable UPnP SDK][Portable UPnP SDK] | | 22.1.7 | 2026-09-29 | [Portable UPnP SDK][Portable UPnP SDK] | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/docs/Doxyfile new/libupnp-22.2.0/docs/Doxyfile --- old/libupnp-22.1.10/docs/Doxyfile 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/docs/Doxyfile 2026-11-04 14:48:44.000000000 +0100 @@ -38,7 +38,7 @@ # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 22.1.10 +PROJECT_NUMBER = 22.2.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.1.10/gtest/CMakeLists.txt new/libupnp-22.2.0/gtest/CMakeLists.txt --- old/libupnp-22.1.10/gtest/CMakeLists.txt 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/gtest/CMakeLists.txt 2026-11-04 14:48:44.000000000 +0100 @@ -8,6 +8,12 @@ UPNP_addGTest(test_ssl_ctx test_ssl_ctx.cpp) endif() +# regression test for GHSA-8rmx-x2cq-xh33 — server authentication; the test +# server uses POSIX sockets. +if(UPNP_ENABLE_OPEN_SSL AND NOT WIN32) + UPNP_addGTest(test_ssl_verify test_ssl_verify.cpp) +endif() + #UPNP_addGTest (test_UpnpHttpHeaderList test_UpnpHttpHeaderList.cpp # ADDITIONAL_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/../upnp/src/threadutil/ #) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/gtest/test_ssl_verify.cpp new/libupnp-22.2.0/gtest/test_ssl_verify.cpp --- old/libupnp-22.1.10/gtest/test_ssl_verify.cpp 1970-01-01 01:00:00.000000000 +0100 +++ new/libupnp-22.2.0/gtest/test_ssl_verify.cpp 2026-11-04 14:48:44.000000000 +0100 @@ -0,0 +1,330 @@ +// regression: GHSA-8rmx-x2cq-xh33 +// +// The HTTPS client must authenticate the server: the certificate chain is +// verified by default and the certificate must match the host of the URL. +// HTTP paths that have no TLS must not send an https:// request in clear. + +#include "upnp.h" + +#include "gtest/gtest.h" + +#ifdef UPNP_ENABLE_OPEN_SSL + + #include <openssl/x509v3.h> + + #include <arpa/inet.h> + #include <netdb.h> + #include <netinet/in.h> + #include <poll.h> + #include <signal.h> + #include <sys/socket.h> + #include <sys/time.h> + #include <unistd.h> + + #include <atomic> + #include <cstring> + #include <string> + #include <thread> + +namespace +{ + +EVP_PKEY *make_key() +{ + EVP_PKEY *pkey = nullptr; + EVP_PKEY_CTX *kctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr); + if (kctx && EVP_PKEY_keygen_init(kctx) == 1 && + EVP_PKEY_CTX_set_ec_paramgen_curve_nid( + kctx, NID_X9_62_prime256v1) == 1) { + EVP_PKEY_keygen(kctx, &pkey); + } + EVP_PKEY_CTX_free(kctx); + return pkey; +} + +/* Self-signed certificate with the given subjectAltName. */ +X509 *make_cert(EVP_PKEY *pkey, const char *san) +{ + X509 *x = X509_new(); + X509_set_version(x, 2); + ASN1_INTEGER_set(X509_get_serialNumber(x), 1); + X509_gmtime_adj(X509_getm_notBefore(x), -60); + X509_gmtime_adj(X509_getm_notAfter(x), 3600); + X509_set_pubkey(x, pkey); + X509_NAME *name = X509_NAME_new(); + X509_NAME_add_entry_by_txt(name, + "CN", + MBSTRING_ASC, + (const unsigned char *)"pupnp-test", + -1, + -1, + 0); + X509_set_subject_name(x, name); + X509_set_issuer_name(x, name); + X509_NAME_free(name); + X509V3_CTX v3; + X509V3_set_ctx_nodb(&v3); + X509V3_set_ctx(&v3, x, x, nullptr, nullptr, 0); + X509_EXTENSION *ext = X509V3_EXT_conf_nid( + nullptr, &v3, NID_subject_alt_name, (char *)san); + X509_add_ext(x, ext, -1); + X509_EXTENSION_free(ext); + X509_sign(x, pkey, EVP_sha256()); + return x; +} + +/* The address pupnp connects to for a host name (first usable result). */ +int loopback_family(const char *host) +{ + struct addrinfo hints; + struct addrinfo *res = nullptr; + int family = AF_INET; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + if (getaddrinfo(host, nullptr, &hints, &res) == 0) { + for (struct addrinfo *r = res; r; r = r->ai_next) { + if (r->ai_family == AF_INET || + r->ai_family == AF_INET6) { + family = r->ai_family; + break; + } + } + freeaddrinfo(res); + } + return family; +} + +/* One-shot TLS server on a loopback address. */ +class TlsServer +{ +public: + std::atomic<bool> handshake_ok{false}; + std::atomic<bool> plaintext{false}; + unsigned short port = 0; + + bool start(X509 *cert, EVP_PKEY *pkey, int family = AF_INET) + { + family_ = family; + ctx_ = SSL_CTX_new(TLS_server_method()); + if (!ctx_ || SSL_CTX_use_certificate(ctx_, cert) != 1 || + SSL_CTX_use_PrivateKey(ctx_, pkey) != 1) + return false; + lfd_ = socket(family, SOCK_STREAM, 0); + if (lfd_ < 0) + return false; + struct sockaddr_storage ss; + socklen_t len; + memset(&ss, 0, sizeof(ss)); + if (family == AF_INET6) { + struct sockaddr_in6 *a = (struct sockaddr_in6 *)&ss; + a->sin6_family = AF_INET6; + a->sin6_addr = in6addr_loopback; + len = sizeof(*a); + } else { + struct sockaddr_in *a = (struct sockaddr_in *)&ss; + a->sin_family = AF_INET; + a->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + len = sizeof(*a); + } + if (bind(lfd_, (struct sockaddr *)&ss, len) != 0 || + listen(lfd_, 1) != 0 || + getsockname(lfd_, (struct sockaddr *)&ss, &len) != 0) + return false; + port = ntohs(family == AF_INET6 + ? ((struct sockaddr_in6 *)&ss)->sin6_port + : ((struct sockaddr_in *)&ss)->sin_port); + thread_ = std::thread(&TlsServer::serve, this); + return true; + } + + /* Wakes the server if the client never connected, then joins it. */ + void stop() + { + if (thread_.joinable()) { + stopping_ = true; + int fd = socket(family_, SOCK_STREAM, 0); + struct sockaddr_storage ss; + socklen_t len = sizeof(ss); + getsockname(lfd_, (struct sockaddr *)&ss, &len); + if (connect(fd, (struct sockaddr *)&ss, len) != 0) { + /* The server already exited. */ + } + thread_.join(); + close(fd); + } + if (lfd_ >= 0) + close(lfd_); + lfd_ = -1; + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + + ~TlsServer() { stop(); } + +private: + void serve() + { + struct pollfd p = {lfd_, POLLIN, 0}; + if (poll(&p, 1, 10000) != 1) + return; + int fd = accept(lfd_, nullptr, nullptr); + if (fd < 0) + return; + if (stopping_) { + close(fd); + return; + } + struct timeval tv = {5, 0}; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + unsigned char first = 0; + if (recv(fd, &first, 1, MSG_PEEK) == 1 && first != 0x16) + plaintext = true; /* not a TLS handshake record */ + SSL *ssl = SSL_new(ctx_); + SSL_set_fd(ssl, fd); + if (SSL_accept(ssl) == 1) { + handshake_ok = true; + char buf[256]; + while (SSL_read(ssl, buf, sizeof(buf)) > 0) { + } + } + SSL_free(ssl); + close(fd); + } + + SSL_CTX *ctx_ = nullptr; + int lfd_ = -1; + int family_ = AF_INET; + std::atomic<bool> stopping_{false}; + std::thread thread_; +}; + +class SslVerifyTest : public ::testing::Test +{ +protected: + void SetUp() override + { + pkey_ = make_key(); + ASSERT_NE(nullptr, pkey_); + ASSERT_EQ(UPNP_E_SUCCESS, + UpnpInitSslContext(0, TLS_client_method())); + } + + void TearDown() override + { + server_.stop(); + UpnpSetSslCtx(nullptr); + X509_free(cert_); + EVP_PKEY_free(pkey_); + } + + void serve(const char *san, int family = AF_INET) + { + cert_ = make_cert(pkey_, san); + ASSERT_NE(nullptr, cert_); + ASSERT_TRUE(server_.start(cert_, pkey_, family)); + } + + void trust_server_cert() + { + X509_STORE *store = SSL_CTX_get_cert_store(UpnpGetSslCtx()); + ASSERT_EQ(1, X509_STORE_add_cert(store, cert_)); + } + + std::string url(const char *host) + { + return std::string("https://") + host + ":" + + std::to_string(server_.port) + "/"; + } + + int open(const std::string &u) + { + void *handle = nullptr; + int ret = UpnpOpenHttpConnection(u.c_str(), &handle, 5); + if (handle) + UpnpCloseHttpConnection(handle); + server_.stop(); + return ret; + } + + EVP_PKEY *pkey_ = nullptr; + X509 *cert_ = nullptr; + TlsServer server_; +}; + +} // namespace + +TEST_F(SslVerifyTest, UntrustedCertificateIsRejected) +{ + serve("IP:127.0.0.1"); + EXPECT_NE(UPNP_E_SUCCESS, open(url("127.0.0.1"))); + EXPECT_FALSE(server_.handshake_ok); +} + +TEST_F(SslVerifyTest, TrustedCertificateForAnotherIpIsRejected) +{ + serve("IP:127.0.0.2"); + trust_server_cert(); + EXPECT_NE(UPNP_E_SUCCESS, open(url("127.0.0.1"))); + EXPECT_FALSE(server_.handshake_ok); +} + +TEST_F(SslVerifyTest, TrustedCertificateForAnotherNameIsRejected) +{ + serve("DNS:not-the-host", loopback_family("localhost")); + trust_server_cert(); + EXPECT_NE(UPNP_E_SUCCESS, open(url("localhost"))); + EXPECT_FALSE(server_.handshake_ok); +} + +TEST_F(SslVerifyTest, TrustedCertificateForTheIpIsAccepted) +{ + serve("IP:127.0.0.1"); + trust_server_cert(); + EXPECT_EQ(UPNP_E_SUCCESS, open(url("127.0.0.1"))); + EXPECT_TRUE(server_.handshake_ok); +} + +TEST_F(SslVerifyTest, TrustedCertificateForTheNameIsAccepted) +{ + serve("DNS:localhost", loopback_family("localhost")); + trust_server_cert(); + EXPECT_EQ(UPNP_E_SUCCESS, open(url("localhost"))); + EXPECT_TRUE(server_.handshake_ok); +} + +/* An application may opt out of verification on the context it owns. */ +TEST_F(SslVerifyTest, ApplicationCanDisableVerification) +{ + serve("IP:127.0.0.2"); + SSL_CTX_set_verify(UpnpGetSslCtx(), SSL_VERIFY_NONE, nullptr); + EXPECT_EQ(UPNP_E_SUCCESS, open(url("127.0.0.1"))); + EXPECT_TRUE(server_.handshake_ok); +} + +/* Paths without TLS must not send an https:// request in clear text. */ +TEST_F(SslVerifyTest, DownloadOfHttpsUrlIsNotSentInClear) +{ + serve("IP:127.0.0.1"); + trust_server_cert(); + char *buf = nullptr; + char content_type[LINE_SIZE]; + int ret = UpnpDownloadUrlItem( + url("127.0.0.1").c_str(), &buf, content_type); + free(buf); + server_.stop(); + EXPECT_FALSE(server_.plaintext); + EXPECT_EQ(UPNP_E_INVALID_URL, ret); +} + +#endif /* UPNP_ENABLE_OPEN_SSL */ + +int main(int argc, char **argv) +{ +#ifdef UPNP_ENABLE_OPEN_SSL + /* The test server writes to clients that abort the handshake. */ + signal(SIGPIPE, SIG_IGN); +#endif + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/inc/upnp.h new/libupnp-22.2.0/upnp/inc/upnp.h --- old/libupnp-22.1.10/upnp/inc/upnp.h 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/inc/upnp.h 2026-11-04 14:48:44.000000000 +0100 @@ -555,6 +555,13 @@ * \brief Initializes the OpenSSL library, and the OpenSSL context for use * with pupnp * + * The context authenticates HTTPS servers: it uses \c SSL_VERIFY_PEER with + * the system's default trust store, and every connection checks that the + * server certificate matches the host of the URL (DNS name or IP address). + * A server with a self-signed or otherwise untrusted certificate is + * rejected. To trust it, add its certificate or CA to the context returned by + * UpnpGetSslCtx() (e.g. with \c SSL_CTX_load_verify_locations()). + * * \note This method is only enabled if pupnp is compiled with open-ssl support. * * \return An integer representing one of the following: @@ -580,6 +587,11 @@ * Allows the application to configure the context after * UpnpInitSslContext() has created it (e.g. load certificates and keys). * + * pupnp honors the verify mode of the context. Setting \c SSL_VERIFY_NONE + * with \c SSL_CTX_set_verify() disables server authentication, including + * the host name check, and exposes HTTPS connections to man-in-the-middle + * attacks. + * * \note This method is only enabled if pupnp is compiled with open-ssl support. * * \return The SSL_CTX pointer, or NULL if not yet initialized. @@ -598,6 +610,13 @@ * When \b ctx is NULL, the current context is freed and cleared so that * a new one can be set. * + * pupnp uses \b ctx as given and honors its verify mode. A context from + * \c SSL_CTX_new() does not verify the server (\c SSL_VERIFY_NONE); call + * \c SSL_CTX_set_verify() with \c SSL_VERIFY_PEER and load a trust store + * (e.g. \c SSL_CTX_set_default_verify_paths()) to authenticate HTTPS servers. + * With \c SSL_VERIFY_PEER, the server certificate must also match the host + * of the URL. + * * \note This method is only enabled if pupnp is compiled with open-ssl support. * * \return An integer representing one of the following: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/src/api/upnpapi.c new/libupnp-22.2.0/upnp/src/api/upnpapi.c --- old/libupnp-22.1.10/upnp/src/api/upnpapi.c 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/src/api/upnpapi.c 2026-11-04 14:48:44.000000000 +0100 @@ -622,6 +622,13 @@ if (!gSslCtx) { return UPNP_E_INIT_FAILED; } + /* Authenticate the server against the system trust store. */ + SSL_CTX_set_verify(gSslCtx, SSL_VERIFY_PEER, NULL); + if (SSL_CTX_set_default_verify_paths(gSslCtx) != 1) { + SSL_CTX_free(gSslCtx); + gSslCtx = NULL; + return UPNP_E_INIT_FAILED; + } return UPNP_E_SUCCESS; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/src/genlib/net/http/httpparser.c new/libupnp-22.2.0/upnp/src/genlib/net/http/httpparser.c --- old/libupnp-22.1.10/upnp/src/genlib/net/http/httpparser.c 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/src/genlib/net/http/httpparser.c 2026-11-04 14:48:44.000000000 +0100 @@ -919,8 +919,10 @@ if (case_sensitive) { return c == scan_char ? PARSE_OK : PARSE_NO_MATCH; } else { - return tolower(c) == tolower(scan_char) ? PARSE_OK - : PARSE_NO_MATCH; + return tolower((unsigned char)c) == + tolower((unsigned char)scan_char) + ? PARSE_OK + : PARSE_NO_MATCH; } } @@ -1462,10 +1464,10 @@ /* skip 3 ints */ for (i = 0; i < 3; i++) { /* go to start of num */ - while (!isdigit(*p)) + while (!isdigit((unsigned char)*p)) p++; /* skip int */ - while (isdigit(*p)) + while (isdigit((unsigned char)*p)) p++; } /* whitespace must exist after status code */ @@ -2339,7 +2341,8 @@ /* Make it lowercase */ for (i = 0; raw_value->buf[i]; ++i) { - raw_value->buf[i] = (char)tolower(raw_value->buf[i]); + raw_value->buf[i] = + (char)tolower((unsigned char)raw_value->buf[i]); } /* null-terminate */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/src/genlib/net/http/httpreadwrite.c new/libupnp-22.2.0/upnp/src/genlib/net/http/httpreadwrite.c --- old/libupnp-22.1.10/upnp/src/genlib/net/http/httpreadwrite.c 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/src/genlib/net/http/httpreadwrite.c 2026-11-04 14:48:44.000000000 +0100 @@ -285,6 +285,10 @@ char errorBuffer[ERROR_BUFFER_LEN]; http_FixUrl(destination_url, url); + /* No TLS on this path: never send an https request in clear. */ + if (token_string_casecmp(&url->scheme, "https") == 0) { + return (SOCKET)(UPNP_E_INVALID_URL); + } http_socket = socket((int)url->hostport.IPaddress.ss_family, SOCK_STREAM, 0); @@ -820,6 +824,11 @@ int http_error_code; SOCKINFO info; + /* No TLS on this path: never send an https request in clear. */ + if (token_string_casecmp(&destination->scheme, "https") == 0) { + parser_response_init(response, req_method); + return UPNP_E_INVALID_URL; + } tcp_connection = socket( (int)destination->hostport.IPaddress.ss_family, SOCK_STREAM, 0); if (tcp_connection == INVALID_SOCKET) { @@ -1323,6 +1332,36 @@ return UPNP_E_SUCCESS; } +#ifdef UPNP_ENABLE_OPEN_SSL +/*! + * \brief Copies the host of a "host[:port]" or "[v6]:port" token, without + * port or brackets, as a NUL-terminated string. + * + * \return 0 on success, -1 if the host is empty or does not fit. + */ +static int get_tls_host(const token *hostport, char *host, size_t size) +{ + const char *p = hostport->buff; + size_t len = hostport->size; + size_t n = 0; + + if (len > 0 && p[0] == '[') { + p++; + len--; + while (n < len && p[n] != ']') + n++; + } else { + while (n < len && p[n] != ':') + n++; + } + if (n == 0 || n >= size) + return -1; + memcpy(host, p, n); + host[n] = '\0'; + return 0; +} +#endif + int http_OpenHttpConnection(const char *url_str, void **Handle, int timeout) { int ret_code; @@ -1371,7 +1410,13 @@ #ifdef UPNP_ENABLE_OPEN_SSL /* For HTTPS connections start the TLS/SSL handshake. */ if (token_string_casecmp(&url.scheme, "https") == 0) { - ret_code = sock_ssl_connect(&handle->sock_info); + char host[256]; + if (get_tls_host(&url.hostport.text, host, sizeof(host)) != 0) { + sock_destroy(&handle->sock_info, SD_BOTH); + ret_code = UPNP_E_INVALID_URL; + goto errorHandler; + } + ret_code = sock_ssl_connect(&handle->sock_info, host); if (ret_code != UPNP_E_SUCCESS) { sock_destroy(&handle->sock_info, SD_BOTH); goto errorHandler; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/src/genlib/net/http/webserver.c new/libupnp-22.2.0/upnp/src/genlib/net/http/webserver.c --- old/libupnp-22.1.10/upnp/src/genlib/net/http/webserver.c 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/src/genlib/net/http/webserver.c 2026-11-04 14:48:44.000000000 +0100 @@ -758,7 +758,7 @@ char *s) { while (*s) { - *s = (char)toupper(*s); + *s = (char)toupper((unsigned char)*s); ++s; } } @@ -865,7 +865,7 @@ for (i = 0; i < (int)strlen(Tok); i++) { if (Tok[i] == '-') { break; - } else if (isdigit(Tok[i])) { + } else if (isdigit((unsigned char)Tok[i])) { Is_Suffix_byte_Range = 0; break; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/src/genlib/net/sock.c new/libupnp-22.2.0/upnp/src/genlib/net/sock.c --- old/libupnp-22.1.10/upnp/src/genlib/net/sock.c 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/src/genlib/net/sock.c 2026-11-04 14:48:44.000000000 +0100 @@ -63,6 +63,7 @@ #ifdef UPNP_ENABLE_OPEN_SSL #include <openssl/ssl.h> + #include <openssl/x509v3.h> #endif #ifndef MSG_NOSIGNAL @@ -102,13 +103,28 @@ } #ifdef UPNP_ENABLE_OPEN_SSL -int sock_ssl_connect(SOCKINFO *info) +int sock_ssl_connect(SOCKINFO *info, const char *host) { int status = 0; + ASN1_OCTET_STRING *ip; + info->ssl = SSL_new(gSslCtx); if (!info->ssl) { return UPNP_E_SOCKET_ERROR; } + /* Bind the expected peer identity to this connection. */ + ip = a2i_IPADDRESS(host); + if (ip) { + ASN1_OCTET_STRING_free(ip); + status = X509_VERIFY_PARAM_set1_ip_asc( + SSL_get0_param(info->ssl), host); + } else { + status = SSL_set_tlsext_host_name(info->ssl, host) == 1 && + SSL_set1_host(info->ssl, host) == 1; + } + if (status != 1) { + return UPNP_E_SOCKET_ERROR; + } status = SSL_set_fd(info->ssl, info->socket); if (status == 1) { status = SSL_connect(info->ssl); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/src/genlib/net/uri/uri.c new/libupnp-22.2.0/upnp/src/genlib/net/uri/uri.c --- old/libupnp-22.1.10/upnp/src/genlib/net/uri/uri.c 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/src/genlib/net/uri/uri.c 2026-11-04 14:48:44.000000000 +0100 @@ -110,7 +110,7 @@ /*! [in] Char to be matched for UNRESERVED characters. */ char in) { - if (isalnum(in) || is_mark(in)) { + if (isalnum((unsigned char)in) || is_mark(in)) { return 1; } else { return 0; @@ -129,7 +129,8 @@ /*! [in] Char sequence to be matched for ESCAPED characters. */ const char *in) { - if (in[0] == '%' && isxdigit(in[1]) && isxdigit(in[2])) { + if (in[0] == '%' && isxdigit((unsigned char)in[1]) && + isxdigit((unsigned char)in[2])) { return 1; } else { return 0; @@ -143,8 +144,9 @@ size_t i = (size_t)0; size_t j = (size_t)0; - if (in[index] == '%' && isxdigit(in[index + (size_t)1]) && - isxdigit(in[index + (size_t)2])) { + if (in[index] == '%' && + isxdigit((unsigned char)in[index + (size_t)1]) && + isxdigit((unsigned char)in[index + (size_t)2])) { /* Note the "%2x", makes sure that we convert a maximum of two * characters. */ #ifdef _WIN32 @@ -364,7 +366,8 @@ /* IPv4 address -OR- host name. */ srv_name = c; while (*c != ':' && *c != '/' && - (isalnum(*c) || *c == '.' || *c == '-')) { + (isalnum((unsigned char)*c) || *c == '.' || + *c == '-')) { if (*c == '.') last_dot = c; c++; @@ -374,7 +377,7 @@ *c = '\0'; if (has_port == 1) c++; - if (last_dot != NULL && isdigit(*(last_dot + 1))) + if (last_dot != NULL && isdigit((unsigned char)*(last_dot + 1))) /* Must be an IPv4 address. */ af = AF_INET; /* Otherwise it's a host name: leave IPaddress unresolved @@ -388,7 +391,7 @@ if (has_port == 1) { /* Port is specified. */ srv_port = c; - while (*c != '\0' && isdigit(*c)) { + while (*c != '\0' && isdigit((unsigned char)*c)) { c++; } long port_l = strtol(srv_port, NULL, 10); @@ -513,13 +516,13 @@ out->size = (size_t)0; out->buff = NULL; - if ((max == (size_t)0) || (!isalpha(in[0]))) + if ((max == (size_t)0) || (!isalpha((unsigned char)in[0]))) return (size_t)0; i++; while ((i < max) && (in[i] != ':')) { - if (!(isalnum(in[i]) || (in[i] == '+') || (in[i] == '-') || - (in[i] == '.'))) + if (!(isalnum((unsigned char)in[i]) || (in[i] == '+') || + (in[i] == '-') || (in[i] == '.'))) return (size_t)0; i++; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/src/inc/sock.h new/libupnp-22.2.0/upnp/src/inc/sock.h --- old/libupnp-22.1.10/upnp/src/inc/sock.h 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/src/inc/sock.h 2026-11-04 14:48:44.000000000 +0100 @@ -124,6 +124,9 @@ * \brief Associates an SSL object with the socket and begins * the client-side SSL/TLS handshake. * + * The server certificate must match \b host (a DNS name or an IP literal); + * the check takes effect when the context verifies the peer. + * * \return Integer: * \li \c UPNP_E_SUCCESS * \li \c UPNP_E_SOCKET_ERROR @@ -131,7 +134,9 @@ #ifdef UPNP_ENABLE_OPEN_SSL int sock_ssl_connect( /*! [out] Socket Information Object. */ - SOCKINFO *info); + SOCKINFO *info, + /*! [in] Host of the URL, without port or brackets. */ + const char *host); #endif /*! diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/src/inet_pton.c new/libupnp-22.2.0/upnp/src/inet_pton.c --- old/libupnp-22.1.10/upnp/src/inet_pton.c 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/src/inet_pton.c 2026-11-04 14:48:44.000000000 +0100 @@ -229,7 +229,7 @@ curtok = src; saw_xdigit = 0; val = 0; - while ((ch = tolower(*src++)) != '\0') { + while ((ch = tolower((unsigned char)*src++)) != '\0') { const char *pch; pch = strchr(xdigits, ch); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/test/test_parse_uri.c new/libupnp-22.2.0/upnp/test/test_parse_uri.c --- old/libupnp-22.1.10/upnp/test/test_parse_uri.c 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/test/test_parse_uri.c 2026-11-04 14:48:44.000000000 +0100 @@ -234,6 +234,52 @@ return 0; } +/* + * Regression test for PR #666: bytes from 0x80 to 0xFF in each part of a + * URL reach the <ctype.h> calls of uri.c, which test_parse_uri_impl.c wraps + * to count arguments outside the unsigned char range. The parse result does + * not matter here, only that no call received a negative value. + */ +extern int ctype_bad_args; + +static int check_high_bytes_ctype(void) +{ + static const char *const urls[] = { + "\xe9" + "ttp://192.0.2.1/", /* parse_scheme(): isalpha */ + "h\xe9" + "tp://192.0.2.1/", /* parse_scheme(): isalnum */ + "http://h\xe9" + "st/", /* parse_hostport(): isalnum */ + "http://192.0.2.\xe9/", /* parse_hostport(): isdigit */ + "http://192.0.2.1:8\xe9/", /* parse_hostport(): port */ + "http://192.0.2.1/%\xe9" + "0", /* is_escaped(): isxdigit */ + "http://192.0.2.1/%0\xe9", /* is_escaped(): isxdigit */ + "http://192.0.2.1/\x80\xe9", /* is_unreserved(): isalnum */ + }; + uri_type url; + int i; + int failures = 0; + + for (i = 0; i < (int)(sizeof(urls) / sizeof(urls[0])); i++) { + ctype_bad_args = 0; + (void)parse_uri(urls[i], strlen(urls[i]), &url); + if (ctype_bad_args != 0) { + printf("%s:%d parse_uri(urls[%d]) passed %d negative " + "char(s) to <ctype.h> -- cast to unsigned " + "char\n", + __FILE__, + __LINE__, + i, + ctype_bad_args); + failures++; + } + } + + return failures; +} + int main(void) { int i; @@ -267,6 +313,7 @@ failures += check_literal_ip_is_noop(); failures += check_resolve_hostport_localhost(); failures += check_nul_terminates_uri(); + failures += check_high_bytes_ctype(); #ifdef _WIN32 WSACleanup(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.10/upnp/test/test_parse_uri_impl.c new/libupnp-22.2.0/upnp/test/test_parse_uri_impl.c --- old/libupnp-22.1.10/upnp/test/test_parse_uri_impl.c 2026-10-29 21:17:58.000000000 +0100 +++ new/libupnp-22.2.0/upnp/test/test_parse_uri_impl.c 2026-11-04 14:48:44.000000000 +0100 @@ -10,4 +10,43 @@ * object/dependency naming conflicts while keeping the library API surface * unchanged. */ + +/* + * Regression test for PR #666: the <ctype.h> functions take an int that must + * be representable as unsigned char or equal EOF (C11 7.4). uri.c passed a + * plain char, so a URL byte from 0x80 to 0xFF became a negative int where + * char is signed. glibc tolerates that, so the wrappers below count every + * out-of-range argument instead; test_parse_uri.c fails when the count is + * not zero. <ctype.h> is included first so that its include guard keeps + * uri.c from redefining the names. + */ +#include <ctype.h> +#include <limits.h> +#include <stdio.h> + +int ctype_bad_args = 0; + +static int ctype_arg(int c) +{ + if (c != EOF && (c < 0 || c > UCHAR_MAX)) { + ctype_bad_args++; + return EOF; + } + return c; +} + +static int checked_isalnum(int c) { return isalnum(ctype_arg(c)); } +static int checked_isalpha(int c) { return isalpha(ctype_arg(c)); } +static int checked_isdigit(int c) { return isdigit(ctype_arg(c)); } +static int checked_isxdigit(int c) { return isxdigit(ctype_arg(c)); } + +#undef isalnum +#undef isalpha +#undef isdigit +#undef isxdigit +#define isalnum checked_isalnum +#define isalpha checked_isalpha +#define isdigit checked_isdigit +#define isxdigit checked_isxdigit + #include "../src/genlib/net/uri/uri.c"
