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
1Patches
19bdfbc77e335Merge pull request #1216 from libexpat/attribute-collision-check-dos
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- github.com/libexpat/libexpat/pull/1216nvdExploitIssue TrackingPatch
- www.openwall.com/lists/oss-security/2026/05/11/16nvdMailing ListThird Party Advisory
News mentions
1- Patch Tuesday - May 2026Rapid7 Blog · May 13, 2026