Regex Injection via Doctype Entities
Description
fast-xml-parser is an open source, pure javascript xml parser. fast-xml-parser allows special characters in entity names, which are not escaped or sanitized. Since the entity name is used for creating a regex for searching and replacing entities in the XML body, an attacker can abuse it for denial of service (DoS) attacks. By crafting an entity name that results in an intentionally bad performing regex and utilizing it in the entity replacement step of the parser, this can cause the parser to stall for an indefinite amount of time. This problem has been resolved in v4.2.4. Users are advised to upgrade. Users unable to upgrade should avoid using DOCTYPE parsing by setting the processEntities: false option.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
fast-xml-parsernpm | >= 4.1.3, < 4.2.4 | 4.2.4 |
Affected products
1- Range: >= 4.1.3, < 4.2.4
Patches
239b0e050bb90fix security bug
2 files changed · +31 −1
spec/entities_spec.js+18 −0 modified@@ -376,6 +376,24 @@ describe("XMLParser Entities", function() { expect(result).toEqual(expected); }); + it("should throw error if an entity name contains special char", function() { + const xmlData = ` + <?xml version="1.0" encoding="UTF-8"?> + + <!DOCTYPE note [ + <!ENTITY nj$ "writer;"> + <!ENTITY wr?er "Writer: Donald Duck."> + ]>`; + + const options = { + processEntities: true, + }; + + expect(() =>{ + const parser = new XMLParser(options); + parser.parse(xmlData); + }).toThrowError("Invalid character $ in entity name") + }); }); describe("XMLParser External Entites", function() {
src/xmlparser/DocTypeReader.js+13 −1 modified@@ -19,7 +19,7 @@ function readDocType(xmlData, i){ i += 7; [entityName, val,i] = readEntityExp(xmlData,i+1); if(val.indexOf("&") === -1) //Parameter entities are not supported - entities[ entityName ] = { + entities[ validateEntityName(entityName) ] = { regx : RegExp( `&${entityName};`,"g"), val: val }; @@ -140,4 +140,16 @@ function isNotation(xmlData, i){ return false } +//an entity name should not contains special characters that may be used in regex +//Eg !?\\\/[]$%{}^&*()<> +const specialChar = "!?\\\/[]$%{}^&*()<>"; + +function validateEntityName(name){ + for (let i = 0; i < specialChar.length; i++) { + const ch = specialChar[i]; + if(name.indexOf(ch) !== -1) throw new Error(`Invalid character ${ch} in entity name`); + } + return name; +} + module.exports = readDocType; \ No newline at end of file
a4bdced80369fix #546: Support complex entity value
2 files changed · +142 −71
spec/entities_spec.js+46 −0 modified@@ -532,4 +532,50 @@ describe("XMLParser External Entites", function() { expect(result).toEqual(expected); }); + + fit("should support entites with tags in content", function() { + const xmlData = ` + <?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY Smile " + <rect x='.5' y='.5' width='29' height='39' fill='black' stroke='red'/> + <g transform='translate(0, 5)'> + <circle cx='15' cy='15' r='10' fill='yellow'/> + <circle cx='12' cy='12' r='1.5' fill='black'/> + <circle cx='17' cy='12' r='1.5' fill='black'/> + <path d='M 10 19 L 15 23 20 19' stroke='black' stroke-width='2'/></g>" + > +]> +<svg width="850px" height="700px" version="1.1" xmlns="http://www.w3.org/2000/svg"> +<g transform="matrix(16,0,0,16,0,0)">&Smile;</g></svg> `; + + const expected = { + "?xml": { + "version": "1.0", + "encoding": "utf-8" + }, + "svg": { + "g": { + "#text": " \n \t<rect x='.5' y='.5' width='29' height='39' fill='black' stroke='red'/>\n\t\t<g transform='translate(0, 5)'> \n\t\t\t<circle cx='15' cy='15' r='10' fill='yellow'/>\n\t\t\t<circle cx='12' cy='12' r='1.5' fill='black'/>\n\t\t\t<circle cx='17' cy='12' r='1.5' fill='black'/>\n\t\t\t<path d='M 10 19 L 15 23 20 19' stroke='black' stroke-width='2'/></g>", + "transform": "matrix(16,0,0,16,0,0)" + }, + "width": "850px", + "height": "700px", + "version": "1.1", + "xmlns": "http://www.w3.org/2000/svg" + } + }; + + const options = { + attributeNamePrefix: "", + ignoreAttributes: false, + processEntities: true, + // preserveOrder: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + // console.log(JSON.stringify(result,null,4)); + + expect(result).toEqual(expected); + }); });
src/xmlparser/DocTypeReader.js+96 −71 modified@@ -11,80 +11,34 @@ function readDocType(xmlData, i){ { i = i+9; let angleBracketsCount = 1; - let hasBody = false, entity = false, comment = false; + let hasBody = false, comment = false; let exp = ""; for(;i<xmlData.length;i++){ - if (xmlData[i] === '<' && !comment) { - if( hasBody && - xmlData[i+1] === '!' && - xmlData[i+2] === 'E' && - xmlData[i+3] === 'N' && - xmlData[i+4] === 'T' && - xmlData[i+5] === 'I' && - xmlData[i+6] === 'T' && - xmlData[i+7] === 'Y' - ){ - i += 7; - entity = true; - }else if( hasBody && - xmlData[i+1] === '!' && - xmlData[i+2] === 'E' && - xmlData[i+3] === 'L' && - xmlData[i+4] === 'E' && - xmlData[i+5] === 'M' && - xmlData[i+6] === 'E' && - xmlData[i+7] === 'N' && - xmlData[i+8] === 'T' - ){ - //Not supported - i += 8; - }else if( hasBody && - xmlData[i+1] === '!' && - xmlData[i+2] === 'A' && - xmlData[i+3] === 'T' && - xmlData[i+4] === 'T' && - xmlData[i+5] === 'L' && - xmlData[i+6] === 'I' && - xmlData[i+7] === 'S' && - xmlData[i+8] === 'T' - ){ - //Not supported - i += 8; - }else if( hasBody && - xmlData[i+1] === '!' && - xmlData[i+2] === 'N' && - xmlData[i+3] === 'O' && - xmlData[i+4] === 'T' && - xmlData[i+5] === 'A' && - xmlData[i+6] === 'T' && - xmlData[i+7] === 'I' && - xmlData[i+8] === 'O' && - xmlData[i+9] === 'N' - ){ - //Not supported - i += 9; - }else if( //comment - xmlData[i+1] === '!' && - xmlData[i+2] === '-' && - xmlData[i+3] === '-' - ){ - comment = true; - }else{ - throw new Error("Invalid DOCTYPE"); + if (xmlData[i] === '<' && !comment) { //Determine the tag type + if( hasBody && isEntity(xmlData, i)){ + i += 7; + [entityName, val,i] = readEntityExp(xmlData,i+1); + if(val.indexOf("&") === -1) //Parameter entities are not supported + entities[ entityName ] = { + regx : RegExp( `&${entityName};`,"g"), + val: val + }; } + else if( hasBody && isElement(xmlData, i)) i += 8;//Not supported + else if( hasBody && isAttlist(xmlData, i)) i += 8;//Not supported + else if( hasBody && isNotation(xmlData, i)) i += 9;//Not supported + else if( isComment) comment = true; + else throw new Error("Invalid DOCTYPE"); + angleBracketsCount++; exp = ""; - } else if (xmlData[i] === '>') { + } else if (xmlData[i] === '>') { //Read tag content if(comment){ if( xmlData[i - 1] === "-" && xmlData[i - 2] === "-"){ comment = false; angleBracketsCount--; } }else{ - if(entity) { - parseEntityExp(exp, entities); - entity = false; - } angleBracketsCount--; } if (angleBracketsCount === 0) { @@ -105,14 +59,85 @@ function readDocType(xmlData, i){ return {entities, i}; } -const entityRegex = RegExp("^\\s([a-zA-z0-0]+)[ \t](['\"])([^&]+)\\2"); -function parseEntityExp(exp, entities){ - const match = entityRegex.exec(exp); - if(match){ - entities[ match[1] ] = { - regx : RegExp( `&${match[1]};`,"g"), - val: match[3] - }; +function readEntityExp(xmlData,i){ + //External entities are not supported + // <!ENTITY ext SYSTEM "http://normal-website.com" > + + //Parameter entities are not supported + // <!ENTITY entityname "&anotherElement;"> + + //Internal entities are supported + // <!ENTITY entityname "replacement text"> + + //read EntityName + let entityName = ""; + for (; i < xmlData.length && (xmlData[i] !== "'" && xmlData[i] !== '"' ); i++) { + // if(xmlData[i] === " ") continue; + // else + entityName += xmlData[i]; + } + entityName = entityName.trim(); + if(entityName.indexOf(" ") !== -1) throw new Error("External entites are not supported"); + + //read Entity Value + const startChar = xmlData[i++]; + let val = "" + for (; i < xmlData.length && xmlData[i] !== startChar ; i++) { + val += xmlData[i]; } + return [entityName, val, i]; } + +function isComment(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === '-' && + xmlData[i+3] === '-') return true + return false +} +function isEntity(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === 'E' && + xmlData[i+3] === 'N' && + xmlData[i+4] === 'T' && + xmlData[i+5] === 'I' && + xmlData[i+6] === 'T' && + xmlData[i+7] === 'Y') return true + return false +} +function isElement(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === 'E' && + xmlData[i+3] === 'L' && + xmlData[i+4] === 'E' && + xmlData[i+5] === 'M' && + xmlData[i+6] === 'E' && + xmlData[i+7] === 'N' && + xmlData[i+8] === 'T') return true + return false +} + +function isAttlist(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === 'A' && + xmlData[i+3] === 'T' && + xmlData[i+4] === 'T' && + xmlData[i+5] === 'L' && + xmlData[i+6] === 'I' && + xmlData[i+7] === 'S' && + xmlData[i+8] === 'T') return true + return false +} +function isNotation(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === 'N' && + xmlData[i+3] === 'O' && + xmlData[i+4] === 'T' && + xmlData[i+5] === 'A' && + xmlData[i+6] === 'T' && + xmlData[i+7] === 'I' && + xmlData[i+8] === 'O' && + xmlData[i+9] === 'N') return true + return false +} + module.exports = readDocType; \ No newline at end of file
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-6w63-h3fj-q4vwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-34104ghsaADVISORY
- github.com/NaturalIntelligence/fast-xml-parser/commit/39b0e050bb909e8499478657f84a3076e39ce76cghsax_refsource_MISCWEB
- github.com/NaturalIntelligence/fast-xml-parser/commit/a4bdced80369892ee413bf08e28b78795a2b0d5bghsax_refsource_MISCWEB
- github.com/NaturalIntelligence/fast-xml-parser/security/advisories/GHSA-6w63-h3fj-q4vwghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.