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-23 14:32:57
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libupnp (Old)
 and      /work/SRC/openSUSE:Factory/.libupnp.new.383539 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "libupnp"

Wed Sep 23 14:32:57 2026 rev:51 rq:1379917 version:22.1.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/libupnp/libupnp.changes  2026-09-12 
21:16:14.762709941 +0200
+++ /work/SRC/openSUSE:Factory/.libupnp.new.383539/libupnp.changes      
2026-09-23 14:34:21.250841059 +0200
@@ -1,0 +2,14 @@
+Wed Sep 23 08:08:24 UTC 2026 - Jan Engelhardt <[email protected]>
+
+- Update to release 22.1.2
+  * Fix quadratic-time DOM construction in ixmlParseBufferEx()
+    [GHSA-xr5m-v53v-jfq9]
+
+-------------------------------------------------------------------
+Tue Sep 22 15:21:50 UTC 2026 - Jan Engelhardt <[email protected]>
+
+- Update to release 22.1.1
+  * Fix unauthenticated remote crash in SOAP QueryStateVariable
+    handling. [GHSA-7mx2-6v7x-xhv7]
+
+-------------------------------------------------------------------

Old:
----
  libupnp-22.1.0.tar.bz2

New:
----
  libupnp-22.1.2.tar.bz2

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

Other differences:
------------------
++++++ libupnp.spec ++++++
--- /var/tmp/diff_new_pack.qlKBbF/_old  2026-09-23 14:34:22.259883243 +0200
+++ /var/tmp/diff_new_pack.qlKBbF/_new  2026-09-23 14:34:22.261883326 +0200
@@ -19,7 +19,7 @@
 
 %define pnpver 22
 Name:           libupnp
-Version:        22.1.0
+Version:        22.1.2
 Release:        0
 Summary:        An implementation of Universal Plug and Play (UPnP)
 License:        BSD-3-Clause

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.qlKBbF/_old  2026-09-23 14:34:22.295884748 +0200
+++ /var/tmp/diff_new_pack.qlKBbF/_new  2026-09-23 14:34:22.298884873 +0200
@@ -1,5 +1,5 @@
-mtime: 1789122216
-commit: 7620613e0282ae9d953b9d7c029340a392d984d6b2028c5fdf9a81699046b79f
+mtime: 1790151648
+commit: 91d5869ac7d557af80fd47e55774a1b7e3af5aa5315fa84bde9dd2ca7fd9629e
 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-23 10:20:48.000000000 +0200
@@ -0,0 +1 @@
+.osc

++++++ libupnp-22.1.0.tar.bz2 -> libupnp-22.1.2.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/CMakeLists.txt 
new/libupnp-22.1.2/CMakeLists.txt
--- old/libupnp-22.1.0/CMakeLists.txt   2026-09-10 22:47:00.000000000 +0200
+++ new/libupnp-22.1.2/CMakeLists.txt   2026-09-23 02:23:04.000000000 +0200
@@ -7,7 +7,7 @@
 set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 
 project(PUPNP
-       VERSION 22.1.0
+       VERSION 22.1.2
        LANGUAGES C)
 
 include(GNUInstallDirs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ChangeLog new/libupnp-22.1.2/ChangeLog
--- old/libupnp-22.1.0/ChangeLog        2026-09-10 22:47:00.000000000 +0200
+++ new/libupnp-22.1.2/ChangeLog        2026-09-23 02:23:04.000000000 +0200
@@ -1,4 +1,38 @@
 *******************************************************************************
+Version 22.1.2
+*******************************************************************************
+
+- GHSA-xr5m-v53v-jfq9: Fix quadratic-time DOM construction in
+  ixmlParseBufferEx() allowing pre-auth denial of service. The parser appended
+  every node by walking its parent's child list from the head, resolved the
+  default namespace and every element prefix by walking the whole stack of
+  open elements, and checked every attribute against all the attributes
+  before it. A well-formed SOAP body within the 64000-byte entity limit, sent
+  unauthenticated to any control URL, could burn up to about 2 s of CPU per
+  request (CWE-407). Children are now appended in constant time, the default
+  namespace in effect is recorded on each element as it is opened, and prefix
+  lookups skip the elements that bind no prefix. Elements are limited to 256
+  attributes, namespace declarations included, and at most 256 namespace
+  prefix bindings may be in scope at once; documents above either limit are
+  rejected with IXML_SYNTAX_ERR. Reported by Denis K.
+
+
+*******************************************************************************
+Version 22.1.1
+*******************************************************************************
+
+- GHSA-7mx2-6v7x-xhv7: Fix unauthenticated remote crash in SOAP
+  QueryStateVariable handling. check_soap_request() passed the local name of
+  the first child of the QueryStateVariable action node straight to
+  strcmp(), but ixmlNode_getLocalName() returns NULL for text and CDATA
+  nodes. A single POST to any control URL, with the QueryStateVariable
+  SOAPACTION and a text or CDATA node where <varName> is expected, crashed
+  the whole process (CWE-476) before the device callback ran. Fixed by adding
+  the same NULL check the neighbouring local-name checks already use; such
+  requests are now rejected with HTTP 400. Reported by Alex J.
+
+
+*******************************************************************************
 Version 22.1.0
 *******************************************************************************
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/docs/Doxyfile 
new/libupnp-22.1.2/docs/Doxyfile
--- old/libupnp-22.1.0/docs/Doxyfile    2026-09-10 22:47:00.000000000 +0200
+++ new/libupnp-22.1.2/docs/Doxyfile    2026-09-23 02:23:04.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.1.0
+PROJECT_NUMBER         = 22.1.2
 
 # 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.0/fuzzer/CMakeLists.txt 
new/libupnp-22.1.2/fuzzer/CMakeLists.txt
--- old/libupnp-22.1.0/fuzzer/CMakeLists.txt    2026-09-10 22:47:00.000000000 
+0200
+++ new/libupnp-22.1.2/fuzzer/CMakeLists.txt    2026-09-23 02:23:04.000000000 
+0200
@@ -4,6 +4,7 @@
 add_executable(FuzzIxml FuzzIxml.c)
 add_executable(FuzzHttp FuzzHttp.c)
 add_executable(FuzzServiceTable FuzzServiceTable.c)
+add_executable(FuzzIxmlDom FuzzIxmlDom.c)
 
 if(NOT DEFINED LOCAL_RUN)
        message("LOCAL_RUN is not defined")
@@ -14,6 +15,7 @@
        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})
+       target_link_libraries(FuzzIxmlDom ixml_static ${LIB_FUZZING_ENGINE})
 else()
        message("LOCAL_RUN is ${LOCAL_RUN}")
 
@@ -43,4 +45,10 @@
                ${LIB_IXML_INCLUDE})
        target_link_libraries(FuzzServiceTable PRIVATE
                ${LIB_UPNP} ${LIB_IXML} ${LIB_FUZZING_ENGINE})
+
+       target_include_directories(FuzzIxmlDom PUBLIC
+               ${LIB_UPNP_INCLUDE}
+               ${LIB_IXML_INCLUDE})
+       target_link_libraries(FuzzIxmlDom PRIVATE
+               ${LIB_IXML} ${LIB_FUZZING_ENGINE})
 endif()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/fuzzer/FuzzIxmlDom.c 
new/libupnp-22.1.2/fuzzer/FuzzIxmlDom.c
--- old/libupnp-22.1.0/fuzzer/FuzzIxmlDom.c     1970-01-01 01:00:00.000000000 
+0100
+++ new/libupnp-22.1.2/fuzzer/FuzzIxmlDom.c     2026-09-23 02:23:04.000000000 
+0200
@@ -0,0 +1,152 @@
+#include "ixml.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define MAX_DEPTH 64
+#define MAX_NODES 4096
+
+static void visit_attributes(IXML_Node *nodeptr)
+{
+       IXML_NamedNodeMap *map;
+       unsigned long i;
+       unsigned long len;
+
+       map = ixmlNode_getAttributes(nodeptr);
+       if (!map) {
+               return;
+       }
+
+       len = ixmlNamedNodeMap_getLength(map);
+       for (i = 0; i < len; i++) {
+               IXML_Node *attr = ixmlNamedNodeMap_item(map, i);
+               const DOMString name;
+
+               if (!attr) {
+                       continue;
+               }
+               (void)ixmlNode_getNodeValue(attr);
+               (void)ixmlNode_getLocalName(attr);
+               (void)ixmlNode_getNamespaceURI(attr);
+               (void)ixmlNode_getPrefix(attr);
+
+               name = ixmlNode_getNodeName(attr);
+               if (name) {
+                       (void)ixmlNamedNodeMap_getNamedItem(map, name);
+               }
+       }
+       ixmlNamedNodeMap_free(map);
+}
+
+static void walk(IXML_Node *nodeptr, int depth, unsigned long *budget)
+{
+       IXML_Node *child;
+
+       if (!nodeptr || depth > MAX_DEPTH || *budget == 0) {
+               return;
+       }
+       (*budget)--;
+
+       if (ixmlNode_hasAttributes(nodeptr)) {
+               visit_attributes(nodeptr);
+       }
+
+       for (child = ixmlNode_getFirstChild(nodeptr); child;
+               child = ixmlNode_getNextSibling(child)) {
+               walk(child, depth + 1, budget);
+       }
+}
+
+static void visit_child_list(IXML_Node *nodeptr)
+{
+       IXML_NodeList *children = ixmlNode_getChildNodes(nodeptr);
+       unsigned long i;
+       unsigned long len;
+
+       if (!children) {
+               return;
+       }
+       len = ixmlNodeList_length(children);
+       for (i = 0; i < len; i++) {
+               (void)ixmlNodeList_item(children, i);
+       }
+       ixmlNodeList_free(children);
+}
+
+static void query_document(IXML_Document *doc)
+{
+       IXML_NodeList *list;
+       IXML_Node *first;
+
+       list = ixmlDocument_getElementsByTagName(doc, "*");
+       if (list) {
+               unsigned long i;
+               unsigned long len = ixmlNodeList_length(list);
+
+               for (i = 0; i < len; i++) {
+                       IXML_Node *node = ixmlNodeList_item(list, i);
+                       IXML_Element *element = (IXML_Element *)node;
+                       const DOMString tag;
+
+                       if (!node) {
+                               continue;
+                       }
+                       tag = ixmlElement_getTagName(element);
+                       if (tag) {
+                               (void)ixmlElement_getAttribute(element, tag);
+                               (void)ixmlElement_hasAttribute(element, tag);
+                               (void)ixmlElement_getAttributeNode(
+                                       element, tag);
+                       }
+               }
+               ixmlNodeList_free(list);
+       }
+
+       list = ixmlDocument_getElementsByTagNameNS(doc, "*", "*");
+       if (list) {
+               ixmlNodeList_free(list);
+       }
+
+       first = ixmlNode_getFirstChild((IXML_Node *)doc);
+       if (first) {
+               IXML_Node *clone = ixmlNode_cloneNode(first, 1);
+
+               if (clone) {
+                       (void)ixmlNode_getFirstChild(clone);
+                       (void)ixmlNode_hasChildNodes(clone);
+                       ixmlNode_free(clone);
+               }
+       }
+}
+
+extern int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
+{
+       IXML_Document *doc = NULL;
+       char *xml;
+
+       if (Size < 1 || Size > 65536) {
+               return 0;
+       }
+
+       xml = malloc(Size + 1);
+       if (!xml) {
+               return 0;
+       }
+       memcpy(xml, Data, Size);
+       xml[Size] = '\0';
+
+       if (ixmlParseBufferEx(xml, &doc) == IXML_SUCCESS && doc) {
+               unsigned long budget = MAX_NODES;
+
+               walk((IXML_Node *)doc, 0, &budget);
+               visit_child_list((IXML_Node *)doc);
+               query_document(doc);
+               ixmlDocument_free(doc);
+       }
+
+       free(xml);
+
+       return 0;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ixml/src/inc/ixmlparser.h 
new/libupnp-22.1.2/ixml/src/inc/ixmlparser.h
--- old/libupnp-22.1.0/ixml/src/inc/ixmlparser.h        2026-09-10 
22:47:00.000000000 +0200
+++ new/libupnp-22.1.2/ixml/src/inc/ixmlparser.h        2026-09-23 
02:23:04.000000000 +0200
@@ -48,6 +48,16 @@
 #define ESC_HEX "&#x"
 #define ESC_DEC "&#"
 
+/*! Maximum number of attributes, namespace declarations included, on one
+ * element. Checking a new attribute for duplicates walks the attributes
+ * before it, so without a cap one element costs O(n^2) to parse. */
+#define IXML_MAX_ATTRIBUTES 256
+
+/*! Maximum number of namespace prefix bindings in scope at once. Resolving a
+ * prefix walks the bindings in scope, so without a cap n prefixed elements
+ * cost O(n^2) to parse. */
+#define IXML_MAX_NAMESPACES 256
+
 typedef struct _IXML_NamespaceURI
 {
        char *nsURI;
@@ -62,6 +72,18 @@
        char *namespaceUri;
        IXML_NamespaceURI *pNsURI;
        struct _IXML_ElementStack *nextElement;
+       /*! Nearest frame below this one that defines a default namespace,
+        * NULL if none. */
+       struct _IXML_ElementStack *defaultNsElement;
+       /*! Number of entries in pNsURI. */
+       int numNsURI;
+       /*! Nonzero if namespaceUri is what the frames below resolve prefix
+        * to, so that this frame binds nothing new for it. */
+       int prefixInherited;
+       /*! Nearest frame below this one that binds a prefix, NULL if none. */
+       struct _IXML_ElementStack *nsScopeElement;
+       /*! Number of prefix bindings in the frames below this one. */
+       int nsInScope;
 } IXML_ElementStack;
 
 typedef enum
@@ -84,6 +106,10 @@
        IXML_Node *pNeedPrefixNode;
        IXML_ElementStack *pCurElement;
        IXML_Node *currentNodePtr;
+       /*! Last child of currentNodePtr, NULL if it has none yet. */
+       IXML_Node *lastChild;
+       /*! Number of attributes read so far on currentNodePtr. */
+       int numAttributes;
        PARSER_STATE state;
        int bHasTopLevel;
 } Parser;
@@ -256,6 +282,23 @@
        IXML_Node *src);
 
 /*!
+ * \brief Same as ixmlNode_appendChild(), but in constant time when the
+ * caller knows the current last child of nodeptr.
+ *
+ * The hint is used only if it is still the last child of nodeptr; otherwise
+ * the child list is walked as ixmlNode_appendChild() does.
+ *
+ * \return Same as ixmlNode_appendChild().
+ */
+int ixmlNode_appendChildAfter(
+       /*! [in] The parent node. */
+       IXML_Node *nodeptr,
+       /*! [in] The node to append. */
+       IXML_Node *newChild,
+       /*! [in] The last child of nodeptr, or NULL if unknown. */
+       IXML_Node *lastChild);
+
+/*!
  * \brief Initializes a nodelist
  */
 void ixmlNodeList_init(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ixml/src/ixmlparser.c 
new/libupnp-22.1.2/ixml/src/ixmlparser.c
--- old/libupnp-22.1.0/ixml/src/ixmlparser.c    2026-09-10 22:47:00.000000000 
+0200
+++ new/libupnp-22.1.2/ixml/src/ixmlparser.c    2026-09-23 02:23:04.000000000 
+0200
@@ -1783,6 +1783,7 @@
                        free(pCur->namespaceUri);
                }
                pCur->namespaceUri = safe_strdup(newNode->nodeValue);
+               pCur->prefixInherited = 0;
                if (pCur->namespaceUri == NULL) {
                        ret = IXML_INSUFFICIENT_MEMORY;
                        line = __LINE__;
@@ -1810,6 +1811,7 @@
                                free(pCur->namespaceUri);
                        }
                        pCur->namespaceUri = safe_strdup(newNode->nodeValue);
+                       pCur->prefixInherited = 0;
                        if (pCur->namespaceUri == NULL) {
                                ret = IXML_INSUFFICIENT_MEMORY;
                                line = __LINE__;
@@ -1860,6 +1862,7 @@
                                } else {
                                        pPrevNs->nextNsURI = pNewNs;
                                }
+                               pCur->numNsURI++;
                        } else {
                                /* update the namespace */
                                if (pNs->nsURI != NULL) {
@@ -2167,6 +2170,20 @@
 }
 
 /*!
+ * \brief Counts the namespace prefixes a frame binds.
+ *
+ * Its namespace declarations, plus its own prefix unless that resolved to
+ * what the frames below already bind.
+ */
+static int Parser_numNsBindings(
+       /*! [in] The element stack frame. */
+       const IXML_ElementStack *pCur)
+{
+       return pCur->numNsURI +
+              (pCur->prefix != NULL && !pCur->prefixInherited ? 1 : 0);
+}
+
+/*!
  * \brief Decides whether element's prefix is already defined.
  */
 static int Parser_ElementPrefixDefined(
@@ -2180,6 +2197,8 @@
        IXML_ElementStack *pCur = xmlParser->pCurElement;
        IXML_NamespaceURI *pNsUri;
 
+       /* Frames that bind no prefix would only repeat what the frames below
+        * them resolve to, so they are skipped. */
        while (pCur != NULL) {
                if ((pCur->prefix != NULL) &&
                        (strcmp(pCur->prefix, newNode->prefix) == 0)) {
@@ -2199,7 +2218,7 @@
                        }
                }
 
-               pCur = pCur->nextElement;
+               pCur = pCur->nsScopeElement;
        }
 
        return 0;
@@ -2276,6 +2295,9 @@
        IXML_Attr *attr = NULL;
        int rc = IXML_SUCCESS;
 
+       if (++xmlParser->numAttributes > IXML_MAX_ATTRIBUTES) {
+               return IXML_SYNTAX_ERR;
+       }
        if (isDuplicateAttribute(xmlParser, newNode)) {
                return IXML_SYNTAX_ERR;
        }
@@ -2353,6 +2375,35 @@
 
                pCurElement = xmlParser->pCurElement;
 
+               /* The parent's attributes, where its namespaces are declared,
+                * have all been read, so the default namespace in effect below
+                * it cannot change any more. */
+               if (pCurElement != NULL) {
+                       int numBindings = Parser_numNsBindings(pCurElement);
+
+                       if (pCurElement->prefix == NULL &&
+                               pCurElement->namespaceUri != NULL) {
+                               pNewStackElement->defaultNsElement =
+                                       pCurElement;
+                       } else {
+                               pNewStackElement->defaultNsElement =
+                                       pCurElement->defaultNsElement;
+                       }
+                       if (numBindings > 0) {
+                               pNewStackElement->nsScopeElement = pCurElement;
+                       } else {
+                               pNewStackElement->nsScopeElement =
+                                       pCurElement->nsScopeElement;
+                       }
+                       pNewStackElement->nsInScope =
+                               pCurElement->nsInScope + numBindings;
+                       if (pNewStackElement->nsInScope > IXML_MAX_NAMESPACES) {
+                               Parser_freeElementStackItem(pNewStackElement);
+                               free(pNewStackElement);
+                               return IXML_SYNTAX_ERR;
+                       }
+               }
+
                /* insert the new element into the top of the stack */
                pNewStackElement->nextElement = pCurElement;
                xmlParser->pCurElement = pNewStackElement;
@@ -2385,13 +2436,18 @@
 {
        IXML_ElementStack *pCur = xmlParser->pCurElement;
 
-       while (pCur != NULL) {
-               if ((pCur->prefix == NULL) && (pCur->namespaceUri != NULL)) {
-                       *nsURI = pCur->namespaceUri;
-                       return 1;
-               } else {
-                       pCur = pCur->nextElement;
-               }
+       if (pCur == NULL) {
+               return 0;
+       }
+       /* Only the top frame can still be changing; the default namespace in
+        * effect below it was recorded when it was pushed. */
+       if ((pCur->prefix == NULL) && (pCur->namespaceUri != NULL)) {
+               *nsURI = pCur->namespaceUri;
+               return 1;
+       }
+       if (pCur->defaultNsElement != NULL) {
+               *nsURI = pCur->defaultNsElement->namespaceUri;
+               return 1;
        }
 
        return 0;
@@ -2412,6 +2468,7 @@
 {
        IXML_Element *newElement = NULL;
        char *nsURI = NULL;
+       int prefixInherited = 0;
        int rc = IXML_SUCCESS;
 
        if (xmlParser->bHasTopLevel) {
@@ -2444,6 +2501,7 @@
                } else {
                        /* fill in the namespace */
                        Parser_setElementNamespace(newElement, nsURI);
+                       prefixInherited = 1;
                }
        } else {
                /* does element has default namespace */
@@ -2463,17 +2521,23 @@
                }
        }
 
-       rc = ixmlNode_appendChild(
-               xmlParser->currentNodePtr, (IXML_Node *)newElement);
+       rc = ixmlNode_appendChildAfter(xmlParser->currentNodePtr,
+               (IXML_Node *)newElement,
+               xmlParser->lastChild);
        if (rc != IXML_SUCCESS) {
                ixmlElement_free(newElement);
                return rc;
        }
 
        xmlParser->currentNodePtr = (IXML_Node *)newElement;
+       xmlParser->lastChild = NULL;
+       xmlParser->numAttributes = 0;
 
        /* push element to stack */
        rc = Parser_pushElement(xmlParser, (IXML_Node *)newElement);
+       if (rc == IXML_SUCCESS) {
+               xmlParser->pCurElement->prefixInherited = prefixInherited;
+       }
        return rc;
 }
 
@@ -2553,6 +2617,8 @@
 
        if (strcmp(newNode->nodeName, xmlParser->currentNodePtr->nodeName) ==
                0) {
+               /* The element being closed is the last child of its parent. */
+               xmlParser->lastChild = xmlParser->currentNodePtr;
                xmlParser->currentNodePtr =
                        xmlParser->currentNodePtr->parentNode;
        } else {
@@ -2624,13 +2690,15 @@
                                                goto ErrorHandler;
                                        }
 
-                                       rc = ixmlNode_appendChild(
+                                       rc = ixmlNode_appendChildAfter(
                                                xmlParser->currentNodePtr,
-                                               tempNode);
+                                               tempNode,
+                                               xmlParser->lastChild);
                                        if (rc != IXML_SUCCESS) {
                                                ixmlNode_free(tempNode);
                                                goto ErrorHandler;
                                        }
+                                       xmlParser->lastChild = tempNode;
 
                                        break;
 
@@ -2642,14 +2710,17 @@
                                        if (rc != IXML_SUCCESS) {
                                                goto ErrorHandler;
                                        }
-                                       rc = ixmlNode_appendChild(
+                                       rc = ixmlNode_appendChildAfter(
                                                xmlParser->currentNodePtr,
-                                               (IXML_Node *)cdataSecNode);
+                                               (IXML_Node *)cdataSecNode,
+                                               xmlParser->lastChild);
                                        if (rc != IXML_SUCCESS) {
                                                ixmlNode_free((IXML_Node *)
                                                                cdataSecNode);
                                                goto ErrorHandler;
                                        }
+                                       xmlParser->lastChild =
+                                               (IXML_Node *)cdataSecNode;
                                        break;
 
                                case eATTRIBUTE_NODE:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ixml/src/node.c 
new/libupnp-22.1.2/ixml/src/node.c
--- old/libupnp-22.1.0/ixml/src/node.c  2026-09-10 22:47:00.000000000 +0200
+++ new/libupnp-22.1.2/ixml/src/node.c  2026-09-23 02:23:04.000000000 +0200
@@ -660,6 +660,12 @@
 
 int ixmlNode_appendChild(IXML_Node *nodeptr, IXML_Node *newChild)
 {
+       return ixmlNode_appendChildAfter(nodeptr, newChild, NULL);
+}
+
+int ixmlNode_appendChildAfter(
+       IXML_Node *nodeptr, IXML_Node *newChild, IXML_Node *lastChild)
+{
        IXML_Node *prev = NULL;
        IXML_Node *next = NULL;
 
@@ -688,6 +694,12 @@
        /* if the first child */
        if (!nodeptr->firstChild) {
                nodeptr->firstChild = newChild;
+       } else if (lastChild && lastChild != newChild &&
+                  lastChild->parentNode == nodeptr &&
+                  !lastChild->nextSibling) {
+               /* The caller knows the end of the list: skip the walk. */
+               lastChild->nextSibling = newChild;
+               newChild->prevSibling = lastChild;
        } else {
                prev = nodeptr->firstChild;
                next = prev->nextSibling;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ixml/test/CMakeLists.txt 
new/libupnp-22.1.2/ixml/test/CMakeLists.txt
--- old/libupnp-22.1.0/ixml/test/CMakeLists.txt 2026-09-10 22:47:00.000000000 
+0200
+++ new/libupnp-22.1.2/ixml/test/CMakeLists.txt 2026-09-23 02:23:04.000000000 
+0200
@@ -14,6 +14,10 @@
 ixml_add_unit_test(test-ixml-poc-ghsa-j247-clonenode poc_ghsa_j247_clonenode.c 
"")
 ixml_add_unit_test(test-ixml-poc-ghsa-j247-isancestor 
poc_ghsa_j247_isancestor.c "")
 ixml_add_unit_test(test-ixml-poc-ghsa-q54q poc_ghsa_q54q.c "")
+ixml_add_unit_test(test-ixml-poc-ghsa-xr5m-siblings poc_ghsa_xr5m_siblings.c 
"")
+ixml_add_unit_test(test-ixml-poc-ghsa-xr5m-depth poc_ghsa_xr5m_depth.c "")
+ixml_add_unit_test(test-ixml-poc-ghsa-xr5m-attributes 
poc_ghsa_xr5m_attributes.c "")
+ixml_add_unit_test(test-ixml-poc-ghsa-xr5m-prefix poc_ghsa_xr5m_prefix.c "")
 
 if(NOT UPNP_ENABLE_TESTING_INTEGRATION)
        return()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m.h 
new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m.h
--- old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m.h        1970-01-01 
01:00:00.000000000 +0100
+++ new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m.h        2026-09-23 
02:23:04.000000000 +0200
@@ -0,0 +1,84 @@
+/* Shared helpers for the GHSA-xr5m-v53v-jfq9 regression tests.
+ *
+ * The bug is a complexity bug: the documents parse correctly, only too
+ * slowly. So the tests compare the parse cost of an adversarial document
+ * against a control document with the same number of nodes, where the
+ * quadratic walk cannot grow. Absolute times vary with the machine and the
+ * build type (Debug, sanitizers); the ratio between the two does not. */
+
+#ifndef POC_GHSA_XR5M_H
+#define POC_GHSA_XR5M_H
+
+#include "ixml.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+/* A linear parser keeps the ratio near 1; the quadratic one reaches 50 and
+ * more at the node counts used here. */
+#define XR5M_MAX_RATIO 8.0
+
+/* Repeat each measurement until at least this much CPU time has elapsed, so
+ * that a coarse clock() (15 ms ticks on Windows) does not dominate. */
+#define XR5M_MIN_CLOCKS (CLOCKS_PER_SEC / 10)
+
+/* Append n copies of s at *p. */
+static char *xr5m_repeat(char *p, const char *s, int n)
+{
+       size_t len = strlen(s);
+       int i;
+
+       for (i = 0; i < n; i++) {
+               memcpy(p, s, len);
+               p += len;
+       }
+       return p;
+}
+
+/* Parse buf repeatedly and return the average cost of one parse, in clock()
+ * ticks. Returns a negative value if the document does not parse. */
+static double xr5m_parse_cost(const char *buf)
+{
+       clock_t start = clock();
+       clock_t elapsed;
+       int runs = 0;
+
+       do {
+               IXML_Document *doc = NULL;
+               int rc = ixmlParseBufferEx(buf, &doc);
+               if (rc != IXML_SUCCESS || !doc) {
+                       fprintf(stderr, "parse failed: rc=%d\n", rc);
+                       return -1.0;
+               }
+               ixmlDocument_free(doc);
+               runs++;
+               elapsed = clock() - start;
+       } while (elapsed < XR5M_MIN_CLOCKS);
+
+       return (double)elapsed / runs;
+}
+
+/* Returns 0 if parsing test costs no more than XR5M_MAX_RATIO times parsing
+ * control, 1 otherwise. */
+static int xr5m_check_ratio(
+       const char *name, const char *test, const char *control)
+{
+       double t = xr5m_parse_cost(test);
+       double c = xr5m_parse_cost(control);
+       double ratio;
+
+       if (t < 0 || c <= 0)
+               return 1;
+       ratio = t / c;
+       printf("%s: %.2f ms vs control %.2f ms, ratio %.1f (max %.1f)\n",
+               name,
+               t * 1000.0 / CLOCKS_PER_SEC,
+               c * 1000.0 / CLOCKS_PER_SEC,
+               ratio,
+               XR5M_MAX_RATIO);
+       return ratio > XR5M_MAX_RATIO;
+}
+
+#endif /* POC_GHSA_XR5M_H */
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m_attributes.c 
new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m_attributes.c
--- old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m_attributes.c     1970-01-01 
01:00:00.000000000 +0100
+++ new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m_attributes.c     2026-09-23 
02:23:04.000000000 +0200
@@ -0,0 +1,71 @@
+/* Regression test for GHSA-xr5m-v53v-jfq9 (attribute lists).
+ *
+ * Every attribute the parser reads costs three walks of the element's
+ * attribute list: the duplicate check in isDuplicateAttribute()
+ * (ixml/src/ixmlparser.c), the duplicate check in get_attribute_node() and
+ * the walk to the end of the list in ixmlElement_setAttributeNode_common()
+ * (ixml/src/element.c). n attributes on one element therefore cost O(n^2):
+ * 7 000 of them in a 64 KB SOAP body take about 0.17 s.
+ *
+ * The duplicate checks are required for well-formedness, so the fix caps the
+ * number of attributes per element at 256 and rejects documents above it with
+ * IXML_SYNTAX_ERR. This test checks both sides of the cap. */
+
+#include "ixml.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#define MAX_ATTRIBUTES 256
+
+int main(void);
+
+/* Parse <r a0="" a1="" ... a(n-1)=""/> and return the result code. */
+static int parse_with_attributes(int n)
+{
+       /* " a" + up to 5 digits + "=\"\"" per attribute. */
+       char *buf = malloc((size_t)n * 10 + 8);
+       char *p = buf;
+       IXML_Document *doc = NULL;
+       int rc;
+       int i;
+
+       if (!buf)
+               return IXML_INSUFFICIENT_MEMORY;
+       p += sprintf(p, "<r");
+       for (i = 0; i < n; i++)
+               p += sprintf(p, " a%d=\"\"", i);
+       sprintf(p, "/>");
+
+       rc = ixmlParseBufferEx(buf, &doc);
+       ixmlDocument_free(doc);
+       free(buf);
+       return rc;
+}
+
+int main(void)
+{
+       int failed = 0;
+       int rc;
+
+       rc = parse_with_attributes(MAX_ATTRIBUTES);
+       printf("%d attributes: rc=%d (expected %d)\n",
+               MAX_ATTRIBUTES,
+               rc,
+               IXML_SUCCESS);
+       failed |= rc != IXML_SUCCESS;
+
+       rc = parse_with_attributes(MAX_ATTRIBUTES + 1);
+       printf("%d attributes: rc=%d (expected %d)\n",
+               MAX_ATTRIBUTES + 1,
+               rc,
+               IXML_SYNTAX_ERR);
+       failed |= rc != IXML_SYNTAX_ERR;
+
+       /* The reporter's shape: as many attributes as fit in 64 KB. */
+       rc = parse_with_attributes(7000);
+       printf("7000 attributes: rc=%d (expected %d)\n", rc, IXML_SYNTAX_ERR);
+       failed |= rc != IXML_SYNTAX_ERR;
+
+       return failed;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m_depth.c 
new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m_depth.c
--- old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m_depth.c  1970-01-01 
01:00:00.000000000 +0100
+++ new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m_depth.c  2026-09-23 
02:23:04.000000000 +0200
@@ -0,0 +1,49 @@
+/* Regression test for GHSA-xr5m-v53v-jfq9 (nesting depth).
+ *
+ * Parser_hasDefaultNamespace() in ixml/src/ixmlparser.c walks the whole
+ * stack of open elements on every unprefixed start tag, looking for a default
+ * namespace. When there is none, the walk reaches the bottom of the stack
+ * every time, so n nested elements cost O(n^2) to parse. The fix must find
+ * the default namespace in effect in constant time.
+ *
+ * The control is the same document with a default namespace declared on the
+ * root. Every open element then carries that namespace, the walk stops at the
+ * first frame, and the cost stays linear. */
+
+#include "poc_ghsa_xr5m.h"
+
+#define NESTING_DEPTH 16000
+
+int main(void);
+
+int main(void)
+{
+       size_t len = (size_t)NESTING_DEPTH * 7 + 32;
+       char *plain = malloc(len + 1);
+       char *defns = malloc(len + 1);
+       char *p;
+       int failed;
+
+       if (!plain || !defns)
+               return 1;
+
+       /* <r><a><a>...</a></a></r> */
+       p = xr5m_repeat(plain, "<r>", 1);
+       p = xr5m_repeat(p, "<a>", NESTING_DEPTH);
+       p = xr5m_repeat(p, "</a>", NESTING_DEPTH);
+       p = xr5m_repeat(p, "</r>", 1);
+       *p = '\0';
+
+       /* <r xmlns="u"><a><a>...</a></a></r> */
+       p = xr5m_repeat(defns, "<r xmlns=\"u\">", 1);
+       p = xr5m_repeat(p, "<a>", NESTING_DEPTH);
+       p = xr5m_repeat(p, "</a>", NESTING_DEPTH);
+       p = xr5m_repeat(p, "</r>", 1);
+       *p = '\0';
+
+       failed = xr5m_check_ratio("deep, no namespace", plain, defns);
+
+       free(plain);
+       free(defns);
+       return failed;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m_prefix.c 
new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m_prefix.c
--- old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m_prefix.c 1970-01-01 
01:00:00.000000000 +0100
+++ new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m_prefix.c 2026-09-23 
02:23:04.000000000 +0200
@@ -0,0 +1,95 @@
+/* Regression test for GHSA-xr5m-v53v-jfq9 (namespace prefixes).
+ *
+ * Parser_ElementPrefixDefined() in ixml/src/ixmlparser.c resolves the prefix
+ * of every prefixed start tag by walking the whole stack of open elements,
+ * and every namespace declaration on each of them, until it finds the
+ * prefix. n prefixed elements declared d levels up therefore cost
+ * O(n * d) to parse. The fix must skip the elements that declare nothing,
+ * and caps the namespace declarations in scope at 256 so that the remaining
+ * walk is bounded; documents above the cap are rejected with IXML_SYNTAX_ERR.
+ *
+ * The timing control holds the same elements at the same depth, with the
+ * prefix declared on the direct parent, where the walk stops at once. */
+
+#include "poc_ghsa_xr5m.h"
+
+#define NESTING_DEPTH 16000
+#define NUM_ELEMENTS 16000
+#define MAX_NAMESPACES 256
+
+int main(void);
+
+/* Parse n nested elements, each declaring a new prefix, around one child, and
+ * return the result code. */
+static int parse_with_namespaces(int n)
+{
+       /* "<a xmlns:p" + up to 5 digits + "=\"u\">" + "</a>" per level. */
+       char *buf = malloc((size_t)n * 26 + 16);
+       char *p = buf;
+       IXML_Document *doc = NULL;
+       int rc;
+       int i;
+
+       if (!buf)
+               return IXML_INSUFFICIENT_MEMORY;
+       for (i = 0; i < n; i++)
+               p += sprintf(p, "<a xmlns:p%d=\"u\">", i);
+       p = xr5m_repeat(p, "<b/>", 1);
+       p = xr5m_repeat(p, "</a>", n);
+       *p = '\0';
+
+       rc = ixmlParseBufferEx(buf, &doc);
+       ixmlDocument_free(doc);
+       free(buf);
+       return rc;
+}
+
+int main(void)
+{
+       size_t len = (size_t)NESTING_DEPTH * 7 + (size_t)NUM_ELEMENTS * 6 + 32;
+       char *far = malloc(len + 1);
+       char *near = malloc(len + 1);
+       char *p;
+       int failed = 0;
+       int rc;
+
+       if (!far || !near)
+               return 1;
+
+       /* <r xmlns:p="u"><a>...<a><p:x/>...</a>...</a></r> */
+       p = xr5m_repeat(far, "<r xmlns:p=\"u\">", 1);
+       p = xr5m_repeat(p, "<a>", NESTING_DEPTH);
+       p = xr5m_repeat(p, "<p:x/>", NUM_ELEMENTS);
+       p = xr5m_repeat(p, "</a>", NESTING_DEPTH);
+       p = xr5m_repeat(p, "</r>", 1);
+       *p = '\0';
+
+       /* <r><a>...<a xmlns:p="u"><p:x/>...</a>...</a></r> */
+       p = xr5m_repeat(near, "<r>", 1);
+       p = xr5m_repeat(p, "<a>", NESTING_DEPTH - 1);
+       p = xr5m_repeat(p, "<a xmlns:p=\"u\">", 1);
+       p = xr5m_repeat(p, "<p:x/>", NUM_ELEMENTS);
+       p = xr5m_repeat(p, "</a>", NESTING_DEPTH);
+       p = xr5m_repeat(p, "</r>", 1);
+       *p = '\0';
+
+       failed |= xr5m_check_ratio("prefix declared far up", far, near);
+       free(far);
+       free(near);
+
+       rc = parse_with_namespaces(MAX_NAMESPACES);
+       printf("%d namespaces in scope: rc=%d (expected %d)\n",
+               MAX_NAMESPACES,
+               rc,
+               IXML_SUCCESS);
+       failed |= rc != IXML_SUCCESS;
+
+       rc = parse_with_namespaces(MAX_NAMESPACES + 1);
+       printf("%d namespaces in scope: rc=%d (expected %d)\n",
+               MAX_NAMESPACES + 1,
+               rc,
+               IXML_SYNTAX_ERR);
+       failed |= rc != IXML_SYNTAX_ERR;
+
+       return failed;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m_siblings.c 
new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m_siblings.c
--- old/libupnp-22.1.0/ixml/test/poc_ghsa_xr5m_siblings.c       1970-01-01 
01:00:00.000000000 +0100
+++ new/libupnp-22.1.2/ixml/test/poc_ghsa_xr5m_siblings.c       2026-09-23 
02:23:04.000000000 +0200
@@ -0,0 +1,65 @@
+/* Regression test for GHSA-xr5m-v53v-jfq9 (sibling lists).
+ *
+ * ixmlNode_appendChild() in ixml/src/node.c walks the whole child list to
+ * find its end before every append, and the parser appends every node it
+ * reads through it. A flat list of n siblings therefore costs O(n^2) to
+ * parse: 16 000 empty elements in a 64 KB SOAP body take about 0.25 s, and
+ * alternating text and elements about 2 s. The fix must append in constant
+ * time.
+ *
+ * The control holds the same elements in groups of 64, so no child list is
+ * longer than 64 and the walk cannot grow. */
+
+#include "poc_ghsa_xr5m.h"
+
+#define NUM_ELEMENTS 16000
+#define GROUP_SIZE 64
+
+int main(void);
+
+int main(void)
+{
+       /* The largest document is the control: <g> and </g> per group. */
+       size_t len = (size_t)NUM_ELEMENTS * 4 +
+                    (size_t)(NUM_ELEMENTS / GROUP_SIZE) * 7 + 8;
+       char *flat = malloc(len + 1);
+       char *mixed = malloc(len + 1);
+       char *grouped = malloc(len + 1);
+       char *p;
+       int i;
+       int failed = 0;
+
+       if (!flat || !mixed || !grouped)
+               return 1;
+
+       /* <r><a/><a/>...</r> */
+       p = xr5m_repeat(flat, "<r>", 1);
+       p = xr5m_repeat(p, "<a/>", NUM_ELEMENTS);
+       p = xr5m_repeat(p, "</r>", 1);
+       *p = '\0';
+
+       /* <r>x<a/>x<a/>...</r>: the same node count as flat, half of them text
+        * nodes, which go through ixmlNode_appendChild() too. */
+       p = xr5m_repeat(mixed, "<r>", 1);
+       p = xr5m_repeat(p, "x<a/>", NUM_ELEMENTS / 2);
+       p = xr5m_repeat(p, "</r>", 1);
+       *p = '\0';
+
+       /* <r><g><a/>...</g><g><a/>...</g>...</r> */
+       p = xr5m_repeat(grouped, "<r>", 1);
+       for (i = 0; i < NUM_ELEMENTS / GROUP_SIZE; i++) {
+               p = xr5m_repeat(p, "<g>", 1);
+               p = xr5m_repeat(p, "<a/>", GROUP_SIZE);
+               p = xr5m_repeat(p, "</g>", 1);
+       }
+       p = xr5m_repeat(p, "</r>", 1);
+       *p = '\0';
+
+       failed |= xr5m_check_ratio("flat siblings", flat, grouped);
+       failed |= xr5m_check_ratio("text/element siblings", mixed, grouped);
+
+       free(flat);
+       free(mixed);
+       free(grouped);
+       return failed;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/libupnp.spec 
new/libupnp-22.1.2/libupnp.spec
--- old/libupnp-22.1.0/libupnp.spec     2026-09-10 22:47:00.000000000 +0200
+++ new/libupnp-22.1.2/libupnp.spec     2026-09-23 02:23:04.000000000 +0200
@@ -1,4 +1,4 @@
-Version: 22.1.0
+Version: 22.1.2
 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.1.0/upnp/src/soap/soap_device.c 
new/libupnp-22.1.2/upnp/src/soap/soap_device.c
--- old/libupnp-22.1.0/upnp/src/soap/soap_device.c      2026-09-10 
22:47:00.000000000 +0200
+++ new/libupnp-22.1.2/upnp/src/soap/soap_device.c      2026-09-23 
02:23:04.000000000 +0200
@@ -726,7 +726,7 @@
                        goto error_handler;
                }
                local_name = ixmlNode_getLocalName(varname_node);
-               if (strcmp(local_name, "varName") != 0) {
+               if (NULL == local_name || strcmp(local_name, "varName") != 0) {
                        goto error_handler;
                }
                nametxt_node = ixmlNode_getFirstChild(varname_node);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libupnp-22.1.0/upnp/test/CMakeLists.txt 
new/libupnp-22.1.2/upnp/test/CMakeLists.txt
--- old/libupnp-22.1.0/upnp/test/CMakeLists.txt 2026-09-10 22:47:00.000000000 
+0200
+++ new/libupnp-22.1.2/upnp/test/CMakeLists.txt 2026-09-23 02:23:04.000000000 
+0200
@@ -305,6 +305,23 @@
        )
 endif()
 
+# GHSA-7mx2-6v7x-xhv7: a QueryStateVariable SOAP request with a text or
+# CDATA node where <varName> is expected made check_soap_request() pass a
+# NULL local name to strcmp(), crashing the process.
+UPNP_Add_Unit_Test(test-upnp-ghsa-xhv7 poc_ghsa_xhv7_soap_varname.c)
+if(UPNP_BUILD_SHARED)
+       target_link_libraries(test-upnp-ghsa-xhv7 PRIVATE
+               $<$<BOOL:${SOCKET_LIBRARY}>:${SOCKET_LIBRARY}>
+               $<$<BOOL:${NSL_LIBRARY}>:${NSL_LIBRARY}>
+       )
+endif()
+if(UPNP_BUILD_STATIC)
+       target_link_libraries(test-upnp-ghsa-xhv7-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.1.0/upnp/test/poc_ghsa_xhv7_soap_varname.c 
new/libupnp-22.1.2/upnp/test/poc_ghsa_xhv7_soap_varname.c
--- old/libupnp-22.1.0/upnp/test/poc_ghsa_xhv7_soap_varname.c   1970-01-01 
01:00:00.000000000 +0100
+++ new/libupnp-22.1.2/upnp/test/poc_ghsa_xhv7_soap_varname.c   2026-09-23 
02:23:04.000000000 +0200
@@ -0,0 +1,265 @@
+/* poc_ghsa_xhv7_soap_varname.c
+ *
+ * Regression test for GHSA-7mx2-6v7x-xhv7: unauthenticated remote crash in
+ * SOAP QueryStateVariable handling.
+ *
+ * check_soap_request() in soap_device.c takes the first child of the
+ * <QueryStateVariable> action node and passes its local name straight to
+ * strcmp(). ixmlNode_getLocalName() returns NULL for text and CDATA nodes,
+ * so a request whose action node starts with a text or CDATA node instead
+ * of a <varName> element made the worker thread dereference NULL and took
+ * the whole process down (CWE-476). The request is handled inside the
+ * library, before the device callback runs, and needs no authentication.
+ *
+ * Test cases:
+ *   A. Valid <varName> element -> HTTP 200 (baseline, also run last to
+ *      prove the server survived).
+ *   B. Text node where <varName> is expected -> HTTP 400.
+ *   C. CDATA node where <varName> is expected -> HTTP 400.
+ *
+ * Pre-fix: the process crashes (SIGSEGV) on case B.
+ * Post-fix: B and C are rejected with 400 and the server keeps running.
+ *
+ * regression: GHSA-7mx2-6v7x-xhv7
+ */
+
+#include "upnp.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef _WIN32
+       #include <arpa/inet.h>
+       #include <netinet/in.h>
+       #include <sys/socket.h>
+       #include <sys/time.h>
+       #include <unistd.h>
+#endif
+
+#define CONTROL_URL_PATH "/control/xhv7"
+
+/* Minimal device description with a single service. */
+static const char DEVICE_DESC[] =
+       "<?xml version=\"1.0\"?>"
+       "<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
+       "<specVersion><major>1</major><minor>0</minor></specVersion>"
+       "<device>"
+       "<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>"
+       "<friendlyName>xhv7</friendlyName>"
+       "<manufacturer>Test</manufacturer>"
+       "<modelName>Test</modelName>"
+       "<UDN>uuid:xhv7-0000-0000-0000-000000000000</UDN>"
+       "<serviceList><service>"
+       "<serviceType>urn:schemas-upnp-org:service:Basic:1</serviceType>"
+       "<serviceId>urn:upnp-org:serviceId:Basic</serviceId>"
+       "<SCPDURL>/scpd.xml</SCPDURL>"
+       "<controlURL>" CONTROL_URL_PATH "</controlURL>"
+       "<eventSubURL>/event/xhv7</eventSubURL>"
+       "</service></serviceList>"
+       "</device>"
+       "</root>";
+
+#define SOAP_PREFIX \
+       "<?xml version=\"1.0\"?>" \
+       "<s:Envelope " \
+       "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"; " \
+       "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\";>" \
+       "<s:Body>" \
+       "<u:QueryStateVariable " \
+       "xmlns:u=\"urn:schemas-upnp-org:control-1-0\">"
+
+#define SOAP_SUFFIX \
+       "</u:QueryStateVariable>" \
+       "</s:Body>" \
+       "</s:Envelope>"
+
+static int device_callback(Upnp_EventType t, void *e, void *c)
+{
+       (void)c;
+       if (t == UPNP_CONTROL_GET_VAR_REQUEST) {
+               UpnpStateVarRequest_set_CurrentVal(
+                       (UpnpStateVarRequest *)e, "42");
+       }
+       return 0;
+}
+
+#ifndef _WIN32
+
+/* POST a QueryStateVariable SOAP body to the control URL. Returns the HTTP
+ * status code, or -1 on socket/parse error. */
+static int send_query(
+       const char *server_ip, unsigned short server_port, const char *body)
+{
+       int fd = -1;
+       int status = -1;
+       char req[2048];
+       int req_len;
+       struct sockaddr_in addr;
+       char resp[512];
+       struct timeval tv;
+       ssize_t n;
+
+       req_len = snprintf(req,
+               sizeof(req),
+               "POST " CONTROL_URL_PATH " HTTP/1.1\r\n"
+               "Host: %s:%u\r\n"
+               "Content-Type: text/xml; charset=\"utf-8\"\r\n"
+               "SOAPACTION: "
+               "\"urn:schemas-upnp-org:control-1-0#QueryStateVariable\"\r\n"
+               "Content-Length: %u\r\n"
+               "Connection: close\r\n\r\n"
+               "%s",
+               server_ip,
+               (unsigned)server_port,
+               (unsigned)strlen(body),
+               body);
+       if (req_len < 0 || (size_t)req_len >= sizeof(req)) {
+               fprintf(stderr, "request too large\n");
+               return -1;
+       }
+
+       fd = socket(AF_INET, SOCK_STREAM, 0);
+       if (fd < 0) {
+               perror("socket");
+               goto done;
+       }
+
+       tv.tv_sec = 5;
+       tv.tv_usec = 0;
+       setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+       memset(&addr, 0, sizeof(addr));
+       addr.sin_family = AF_INET;
+       addr.sin_port = htons(server_port);
+       if (inet_pton(AF_INET, server_ip, &addr.sin_addr) != 1) {
+               fprintf(stderr, "inet_pton(%s) failed\n", server_ip);
+               goto done;
+       }
+       if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
+               perror("connect");
+               goto done;
+       }
+
+       if (send(fd, req, (size_t)req_len, 0) < 0) {
+               perror("send");
+               goto done;
+       }
+
+       n = recv(fd, resp, sizeof(resp) - 1, 0);
+       if (n > 0) {
+               resp[n] = '\0';
+               if (sscanf(resp, "HTTP/1.%*d %d", &status) != 1)
+                       status = -1;
+       }
+
+done:
+       if (fd >= 0)
+               close(fd);
+       return status;
+}
+
+static int check_query(const char *name,
+       const char *server_ip,
+       unsigned short server_port,
+       const char *body,
+       int expected)
+{
+       int status = send_query(server_ip, server_port, body);
+
+       printf("%s -> HTTP %d\n", name, status);
+       if (status != expected) {
+               fprintf(stderr,
+                       "%s FAIL: expected HTTP %d, got %d.\n",
+                       name,
+                       expected,
+                       status);
+               return -1;
+       }
+       printf("%s PASS\n", name);
+       return 0;
+}
+
+#endif /* !_WIN32 */
+
+int main(void)
+{
+#ifdef _WIN32
+       puts("SKIP: raw POSIX socket test not supported on Windows.");
+       return EXIT_SUCCESS;
+#else
+       static const char valid_body[] =
+               SOAP_PREFIX "<u:varName>Status</u:varName>" SOAP_SUFFIX;
+       static const char text_body[] = SOAP_PREFIX "Status" SOAP_SUFFIX;
+       static const char cdata_body[] =
+               SOAP_PREFIX "<![CDATA[Status]]>" SOAP_SUFFIX;
+       int rc;
+       UpnpDevice_Handle handle = -1;
+       const char *server_ip;
+       unsigned short server_port;
+       int result = 0;
+
+       rc = UpnpInit2(NULL, 0);
+       if (rc != UPNP_E_SUCCESS) {
+               fprintf(stderr,
+                       "UpnpInit2 failed (%d); skipping (no network?)\n",
+                       rc);
+               return EXIT_SUCCESS;
+       }
+
+       server_ip = UpnpGetServerIpAddress();
+       server_port = UpnpGetServerPort();
+       if (!server_ip || !server_port) {
+               fprintf(stderr, "Could not determine server address\n");
+               UpnpFinish();
+               return EXIT_FAILURE;
+       }
+       printf("Server: %s:%u\n", server_ip, server_port);
+
+       rc = UpnpRegisterRootDevice2(UPNPREG_BUF_DESC,
+               DEVICE_DESC,
+               sizeof(DEVICE_DESC) - 1,
+               1,
+               device_callback,
+               NULL,
+               &handle);
+       if (rc != UPNP_E_SUCCESS) {
+               fprintf(stderr, "UpnpRegisterRootDevice2 failed: %d\n", rc);
+               UpnpFinish();
+               return EXIT_FAILURE;
+       }
+
+       if (check_query("Test A (valid varName, baseline)",
+                   server_ip,
+                   server_port,
+                   valid_body,
+                   200) != 0) {
+               fprintf(stderr, "Baseline failed; skipping (environment?)\n");
+               UpnpUnRegisterRootDevice(handle);
+               UpnpFinish();
+               return EXIT_SUCCESS;
+       }
+       if (check_query("Test B (text node)",
+                   server_ip,
+                   server_port,
+                   text_body,
+                   400) != 0)
+               result = 1;
+       if (check_query("Test C (CDATA node)",
+                   server_ip,
+                   server_port,
+                   cdata_body,
+                   400) != 0)
+               result = 1;
+       if (check_query("Test A (valid varName, server still alive)",
+                   server_ip,
+                   server_port,
+                   valid_body,
+                   200) != 0)
+               result = 1;
+
+       UpnpUnRegisterRootDevice(handle);
+       UpnpFinish();
+       return result ? EXIT_FAILURE : EXIT_SUCCESS;
+#endif /* !_WIN32 */
+}

Reply via email to