VYPR
Low severity2.9NVD Advisory· Published May 10, 2026· Updated May 14, 2026

CVE-2026-45186

CVE-2026-45186

Description

In libexpat before 2.8.1, the computational complexity of attribute name collision checks allows a denial of service via moderately sized crafted XML input.

Affected products

1

Patches

1
9bdfbc77e335

Merge pull request #1216 from libexpat/attribute-collision-check-dos

https://github.com/libexpat/libexpatSebastian PippingMay 10, 2026via nvd-ref
5 files changed · +333 13
  • expat/Changes+12 0 modified
    @@ -29,6 +29,18 @@
     !! THANK YOU!                        Sebastian Pipping -- Berlin, 2026-03-17 !!
     !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
     
    +Release 2.8.1 XXX XXXXX XX XXXX
    +        Security fixes:
    +           #1216  CVE-2026-TODO -- Fix quadratic runtime from attribute name
    +                    collision checks that allowed denial of service attacks
    +                    through moderately sized crafted XML input (CWE-407).
    +                    Please note that a layer of compression around XML can
    +                    significantly reduce the minimum attack payload size.
    +
    +        Special thanks to:
    +            Berkay Eren Ürün
    +            Nick Wellnhofer
    +
     Release 2.8.0 Fri April 24 2026
             Security fixes:
            #47 #1183  CVE-2026-41080 -- The existing hash flooding protection
    
  • expat/lib/xmlparse.c+29 5 modified
    @@ -387,6 +387,7 @@ typedef struct {
       int nDefaultAtts;
       int allocDefaultAtts;
       DEFAULT_ATTRIBUTE *defaultAtts;
    +  HASH_TABLE defaultAttsNames;
     } ELEMENT_TYPE;
     
     typedef struct {
    @@ -3769,6 +3770,8 @@ storeAtts(XML_Parser parser, const ENCODING *enc, const char *attStr,
                                              sizeof(ELEMENT_TYPE));
         if (! elementType)
           return XML_ERROR_NO_MEMORY;
    +    if (! elementType->defaultAttsNames.parser)
    +      hashTableInit(&(elementType->defaultAttsNames), parser);
         if (parser->m_ns && ! setElementTypePrefix(parser, elementType))
           return XML_ERROR_NO_MEMORY;
       }
    @@ -7102,10 +7105,10 @@ defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, XML_Bool isCdata,
       if (value || isId) {
         /* The handling of default attributes gets messed up if we have
            a default which duplicates a non-default. */
    -    int i;
    -    for (i = 0; i < type->nDefaultAtts; i++)
    -      if (attId == type->defaultAtts[i].id)
    -        return 1;
    +    NAMED *const nameFound
    +        = (NAMED *)lookup(parser, &(type->defaultAttsNames), attId->name, 0);
    +    if (nameFound)
    +      return 1;
         if (isId && ! type->idAtt && ! attId->xmlns)
           type->idAtt = attId;
       }
    @@ -7152,6 +7155,12 @@ defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, XML_Bool isCdata,
       att->isCdata = isCdata;
       if (! isCdata)
         attId->maybeTokenized = XML_TRUE;
    +
    +  NAMED *const nameAddedOrFound = (NAMED *)lookup(
    +      parser, &(type->defaultAttsNames), attId->name, sizeof(NAMED));
    +  if (! nameAddedOrFound)
    +    return 0;
    +
       type->nDefaultAtts += 1;
       return 1;
     }
    @@ -7477,6 +7486,7 @@ dtdReset(DTD *p, XML_Parser parser) {
         ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter);
         if (! e)
           break;
    +    hashTableDestroy(&(e->defaultAttsNames));
         if (e->allocDefaultAtts != 0)
           FREE(parser, e->defaultAtts);
       }
    @@ -7518,6 +7528,7 @@ dtdDestroy(DTD *p, XML_Bool isDocEntity, XML_Parser parser) {
         ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter);
         if (! e)
           break;
    +    hashTableDestroy(&(e->defaultAttsNames));
         if (e->allocDefaultAtts != 0)
           FREE(parser, e->defaultAtts);
       }
    @@ -7611,6 +7622,10 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
                                       sizeof(ELEMENT_TYPE));
         if (! newE)
           return 0;
    +
    +    if (! newE->defaultAttsNames.parser)
    +      hashTableInit(&(newE->defaultAttsNames), parser);
    +
         if (oldE->nDefaultAtts) {
           /* Detect and prevent integer overflow.
            * The preprocessor guard addresses the "always false" warning
    @@ -7635,8 +7650,9 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
           newE->prefix = (PREFIX *)lookup(oldParser, &(newDtd->prefixes),
                                           oldE->prefix->name, 0);
         for (i = 0; i < newE->nDefaultAtts; i++) {
    +      const XML_Char *const attributeName = oldE->defaultAtts[i].id->name;
           newE->defaultAtts[i].id = (ATTRIBUTE_ID *)lookup(
    -          oldParser, &(newDtd->attributeIds), oldE->defaultAtts[i].id->name, 0);
    +          oldParser, &(newDtd->attributeIds), attributeName, 0);
           newE->defaultAtts[i].isCdata = oldE->defaultAtts[i].isCdata;
           if (oldE->defaultAtts[i].value) {
             newE->defaultAtts[i].value
    @@ -7645,6 +7661,12 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
               return 0;
           } else
             newE->defaultAtts[i].value = NULL;
    +
    +      NAMED *const nameAddedOrFound = (NAMED *)lookup(
    +          parser, &(newE->defaultAttsNames), attributeName, sizeof(NAMED));
    +      if (! nameAddedOrFound) {
    +        return 0;
    +      }
         }
       }
     
    @@ -8391,6 +8413,8 @@ getElementType(XML_Parser parser, const ENCODING *enc, const char *ptr,
                                    sizeof(ELEMENT_TYPE));
       if (! ret)
         return NULL;
    +  if (! ret->defaultAttsNames.parser)
    +    hashTableInit(&(ret->defaultAttsNames), getRootParserOf(parser, NULL));
       if (ret->name != name)
         poolDiscard(&dtd->pool);
       else {
    
  • expat/tests/basic_tests.c+287 7 modified
    @@ -2491,11 +2491,9 @@ START_TEST(test_attributes) {
                              {XCS("id"), XCS("one")},
                              {NULL, NULL}};
       AttrInfo tag_info[] = {{XCS("c"), XCS("3")}, {NULL, NULL}};
    -  ElementInfo info[] = {{XCS("doc"), 3, XCS("id"), NULL},
    -                        {XCS("tag"), 1, NULL, NULL},
    -                        {NULL, 0, NULL, NULL}};
    -  info[0].attributes = doc_info;
    -  info[1].attributes = tag_info;
    +  ElementInfo info[] = {{XCS("doc"), 3, 0, XCS("id"), doc_info},
    +                        {XCS("tag"), 1, 0, NULL, tag_info},
    +                        {NULL, 0, 0, NULL, NULL}};
     
       XML_Parser parser = XML_ParserCreate(NULL);
       assert_true(parser != NULL);
    @@ -2514,6 +2512,279 @@ START_TEST(test_attributes) {
     }
     END_TEST
     
    +START_TEST(test_duplicate_cdata_attribute) {
    +  /*
    +  https://www.w3.org/TR/xml/#attdecls
    +
    +  Test the following statement from the linked specification:
    +    When more than one definition is provided for the same attribute of a given
    +    element type, the first declaration is binding and later declarations are
    +    ignored.
    +  */
    +
    +  const char *text
    +      = "<!DOCTYPE doc [\n"
    +        "  <!ATTLIST doc attribute CDATA 'expected' attribute CDATA 'ignored'>\n"
    +        "]>\n"
    +        "<doc/>\n";
    +  AttrInfo doc_info[] = {{XCS("attribute"), XCS("expected")}, {NULL, NULL}};
    +  ElementInfo info[]
    +      = {{XCS("doc"), 0, 1, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}};
    +
    +  XML_Parser parser = XML_ParserCreate(NULL);
    +  assert_true(parser != NULL);
    +
    +  ParserAndElementInfo parserAndElementInfos = {
    +      parser,
    +      info,
    +  };
    +
    +  XML_SetStartElementHandler(parser, counting_start_element_handler);
    +  XML_SetUserData(parser, &parserAndElementInfos);
    +
    +  if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE)
    +      != XML_STATUS_OK)
    +    xml_failure(parser);
    +
    +  XML_ParserFree(parser);
    +}
    +END_TEST
    +
    +START_TEST(test_duplicate_id_attribute_1) {
    +  /*
    +  https://www.w3.org/TR/xml/#attdecls
    +
    +  Test the following statement from the linked specification:
    +    When more than one definition is provided for the same attribute of a given
    +    element type, the first declaration is binding and later declarations are
    +    ignored.
    +  */
    +
    +  const char *text
    +      = "<!DOCTYPE doc [\n"
    +        "  <!ATTLIST doc identifier CDATA 'expected' identifier ID #REQUIRED>\n"
    +        "]>\n"
    +        "<doc/>\n";
    +  AttrInfo doc_info[] = {{XCS("identifier"), XCS("expected")}, {NULL, NULL}};
    +  ElementInfo info[]
    +      = {{XCS("doc"), 0, 1, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}};
    +
    +  XML_Parser parser = XML_ParserCreate(NULL);
    +  assert_true(parser != NULL);
    +
    +  ParserAndElementInfo parserAndElementInfos = {
    +      parser,
    +      info,
    +  };
    +
    +  XML_SetStartElementHandler(parser, counting_start_element_handler);
    +  XML_SetUserData(parser, &parserAndElementInfos);
    +
    +  if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE)
    +      != XML_STATUS_OK)
    +    xml_failure(parser);
    +
    +  XML_ParserFree(parser);
    +}
    +END_TEST
    +
    +START_TEST(test_duplicate_id_attribute_2) {
    +  /*
    +  https://www.w3.org/TR/xml/#attdecls
    +
    +  Test the following statement from the linked specification:
    +    When more than one definition is provided for the same attribute of a given
    +    element type, the first declaration is binding and later declarations are
    +    ignored.
    +  */
    +
    +  const char *text
    +      = "<!DOCTYPE doc [\n"
    +        "  <!ATTLIST doc identifier ID #REQUIRED identifier CDATA 'unexpected'>\n"
    +        "]>\n"
    +        "<doc/>\n";
    +  AttrInfo doc_info[] = {{NULL, NULL}};
    +
    +  ElementInfo info[]
    +      = {{XCS("doc"), 0, 0, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}};
    +
    +  XML_Parser parser = XML_ParserCreate(NULL);
    +  assert_true(parser != NULL);
    +
    +  ParserAndElementInfo parserAndElementInfos = {
    +      parser,
    +      info,
    +  };
    +
    +  XML_SetStartElementHandler(parser, counting_start_element_handler);
    +  XML_SetUserData(parser, &parserAndElementInfos);
    +
    +  if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE)
    +      != XML_STATUS_OK)
    +    xml_failure(parser);
    +
    +  XML_ParserFree(parser);
    +}
    +END_TEST
    +
    +START_TEST(test_duplicate_cdata_attribute_multiple_attlistdecl) {
    +  /*
    +  https://www.w3.org/TR/xml/#attdecls
    +
    +  Test the following statement from the linked specification:
    +    When more than one AttlistDecl is provided for a given element type,
    +    the contents of all those provided are merged.
    +  */
    +  const char *text = "<!DOCTYPE doc [\n"
    +                     "  <!ATTLIST doc attribute CDATA 'expected'>\n"
    +                     "  <!ATTLIST doc attribute CDATA 'ignored'>\n"
    +                     "]>\n"
    +                     "<doc/>\n";
    +  AttrInfo doc_info[] = {{XCS("attribute"), XCS("expected")}, {NULL, NULL}};
    +  ElementInfo info[]
    +      = {{XCS("doc"), 0, 1, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}};
    +
    +  XML_Parser parser = XML_ParserCreate(NULL);
    +  assert_true(parser != NULL);
    +
    +  ParserAndElementInfo parserAndElementInfos = {
    +      parser,
    +      info,
    +  };
    +
    +  XML_SetStartElementHandler(parser, counting_start_element_handler);
    +  XML_SetUserData(parser, &parserAndElementInfos);
    +
    +  if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE)
    +      != XML_STATUS_OK)
    +    xml_failure(parser);
    +
    +  XML_ParserFree(parser);
    +}
    +END_TEST
    +
    +START_TEST(test_duplicate_cdata_attribute_multiple_attlistdecl_2) {
    +  /*
    +  https://www.w3.org/TR/xml/#attdecls
    +
    +  Test the following statement from the linked specification:
    +    When more than one AttlistDecl is provided for a given element type,
    +    the contents of all those provided are merged.
    +  */
    +  const char *text = "<!DOCTYPE doc [\n"
    +                     "  <!ATTLIST doc attribute CDATA 'expected_doc'>\n"
    +                     "  <!ATTLIST tag attribute CDATA 'expected_tag'>\n"
    +                     "  <!ATTLIST doc attribute CDATA 'ignored_doc'>\n"
    +                     "]>\n"
    +                     "<doc><tag></tag></doc>\n";
    +  AttrInfo doc_info[] = {{XCS("attribute"), XCS("expected_doc")}, {NULL, NULL}};
    +  AttrInfo tag_info[] = {{XCS("attribute"), XCS("expected_tag")}, {NULL, NULL}};
    +  ElementInfo info[] = {{XCS("doc"), 0, 1, NULL, doc_info},
    +                        {XCS("tag"), 0, 1, NULL, tag_info},
    +                        {NULL, 0, 0, NULL, NULL}};
    +
    +  XML_Parser parser = XML_ParserCreate(NULL);
    +  assert_true(parser != NULL);
    +
    +  ParserAndElementInfo parserAndElementInfos = {
    +      parser,
    +      info,
    +  };
    +
    +  XML_SetStartElementHandler(parser, counting_start_element_handler);
    +  XML_SetUserData(parser, &parserAndElementInfos);
    +
    +  if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE)
    +      != XML_STATUS_OK)
    +    xml_failure(parser);
    +
    +  XML_ParserFree(parser);
    +}
    +END_TEST
    +
    +START_TEST(test_duplicate_cdata_attribute_multiple_attlistdecl_3) {
    +  /*
    +  https://www.w3.org/TR/xml/#attdecls
    +
    +  Test the following statement from the linked specification:
    +    When more than one AttlistDecl is provided for a given element type,
    +    the contents of all those provided are merged.
    +  */
    +  const char *text
    +      = "<!DOCTYPE doc [\n"
    +        "  <!ATTLIST doc attribute CDATA 'expected_doc'>\n"
    +        "  <!ATTLIST tag attribute CDATA 'expected_tag'>\n"
    +        "  <!ATTLIST doc second_attribute CDATA 'second_expected_doc' attribute CDATA 'ignored_doc'>\n"
    +        "]>\n"
    +        "<doc><tag></tag></doc>\n";
    +  AttrInfo doc_info[] = {{XCS("attribute"), XCS("expected_doc")},
    +                         {XCS("second_attribute"), XCS("second_expected_doc")},
    +                         {NULL, NULL}};
    +  AttrInfo tag_info[] = {{XCS("attribute"), XCS("expected_tag")}, {NULL, NULL}};
    +  ElementInfo info[] = {{XCS("doc"), 0, 2, NULL, doc_info},
    +                        {XCS("tag"), 0, 1, NULL, tag_info},
    +                        {NULL, 0, 0, NULL, NULL}};
    +
    +  XML_Parser parser = XML_ParserCreate(NULL);
    +  assert_true(parser != NULL);
    +
    +  ParserAndElementInfo parserAndElementInfos = {
    +      parser,
    +      info,
    +  };
    +
    +  XML_SetStartElementHandler(parser, counting_start_element_handler);
    +  XML_SetUserData(parser, &parserAndElementInfos);
    +
    +  if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE)
    +      != XML_STATUS_OK)
    +    xml_failure(parser);
    +
    +  XML_ParserFree(parser);
    +}
    +END_TEST
    +
    +START_TEST(test_duplicate_id_attribute_multiple_attlistdecl) {
    +  /*
    +  https://www.w3.org/TR/xml/#attdecls
    +
    +  Test the following statement from the linked specification:
    +    When more than one AttlistDecl is provided for a given element type,
    +    the contents of all those provided are merged.
    +  */
    +  const char *text = "<!DOCTYPE doc [\n"
    +                     "  <!ATTLIST doc identifier ID #REQUIRED>\n"
    +                     "  <!ATTLIST tag identifier CDATA 'identifier_tag'>\n"
    +                     "  <!ATTLIST doc identifier CDATA 'ignored'>\n"
    +                     "]>\n"
    +                     "<doc identifier='doc_identity'><tag></tag></doc>\n";
    +  AttrInfo doc_info[]
    +      = {{XCS("identifier"), XCS("doc_identity")}, {NULL, NULL}};
    +  AttrInfo tag_info[]
    +      = {{XCS("identifier"), XCS("identifier_tag")}, {NULL, NULL}};
    +  ElementInfo info[] = {{XCS("doc"), 1, 0, XCS("identifier"), doc_info},
    +                        {XCS("tag"), 0, 1, NULL, tag_info},
    +                        {NULL, 0, 0, NULL, NULL}};
    +
    +  XML_Parser parser = XML_ParserCreate(NULL);
    +  assert_true(parser != NULL);
    +
    +  ParserAndElementInfo parserAndElementInfos = {
    +      parser,
    +      info,
    +  };
    +
    +  XML_SetStartElementHandler(parser, counting_start_element_handler);
    +  XML_SetUserData(parser, &parserAndElementInfos);
    +
    +  if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE)
    +      != XML_STATUS_OK)
    +    xml_failure(parser);
    +
    +  XML_ParserFree(parser);
    +}
    +END_TEST
    +
     /* Test reset works correctly in the middle of processing an internal
      * entity.  Exercises some obscure code in XML_ParserReset().
      */
    @@ -5567,8 +5838,8 @@ START_TEST(test_deep_nested_attribute_entity) {
                (long unsigned)(N_LINES - 1));
     
       AttrInfo doc_info[] = {{XCS("name"), XCS("deepText")}, {NULL, NULL}};
    -  ElementInfo info[] = {{XCS("foo"), 1, NULL, NULL}, {NULL, 0, NULL, NULL}};
    -  info[0].attributes = doc_info;
    +  ElementInfo info[]
    +      = {{XCS("foo"), 1, 0, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}};
     
       XML_Parser parser = XML_ParserCreate(NULL);
       ParserAndElementInfo parserPlusElemenInfo = {parser, info};
    @@ -6399,6 +6670,15 @@ make_basic_test_case(Suite *s) {
       tcase_add_test__ifdef_xml_dtd(tc_basic, test_empty_foreign_dtd);
       tcase_add_test(tc_basic, test_set_base);
       tcase_add_test(tc_basic, test_attributes);
    +  tcase_add_test(tc_basic, test_duplicate_cdata_attribute);
    +  tcase_add_test(tc_basic, test_duplicate_id_attribute_1);
    +  tcase_add_test(tc_basic, test_duplicate_id_attribute_2);
    +  tcase_add_test(tc_basic, test_duplicate_cdata_attribute_multiple_attlistdecl);
    +  tcase_add_test(tc_basic,
    +                 test_duplicate_cdata_attribute_multiple_attlistdecl_2);
    +  tcase_add_test(tc_basic,
    +                 test_duplicate_cdata_attribute_multiple_attlistdecl_3);
    +  tcase_add_test(tc_basic, test_duplicate_id_attribute_multiple_attlistdecl);
       tcase_add_test__if_xml_ge(tc_basic, test_reset_in_entity);
       tcase_add_test(tc_basic, test_resume_invalid_parse);
       tcase_add_test(tc_basic, test_resume_resuspended);
    
  • expat/tests/handlers.c+4 1 modified
    @@ -137,7 +137,7 @@ counting_start_element_handler(void *userData, const XML_Char *name,
         fail("ID does not have the correct name");
         return;
       }
    -  for (i = 0; i < info->attr_count; i++) {
    +  for (i = 0; i < info->attr_count + info->default_attr_count; i++) {
         attr = info->attributes;
         while (attr->name != NULL) {
           if (! xcstrcmp(atts[0], attr->name))
    @@ -155,6 +155,9 @@ counting_start_element_handler(void *userData, const XML_Char *name,
         /* Remember, two entries in atts per attribute (see above) */
         atts += 2;
       }
    +
    +  // Self-test that the test case's list of expected attributes is complete
    +  assert_true(atts[0] == NULL);
     }
     
     void XMLCALL
    
  • expat/tests/handlers.h+1 0 modified
    @@ -88,6 +88,7 @@ typedef struct attrInfo {
     typedef struct elementInfo {
       const XML_Char *name;
       int attr_count;
    +  int default_attr_count;
       const XML_Char *id_name;
       AttrInfo *attributes;
     } ElementInfo;
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

2

News mentions

1