CVE-2021-21366 (published 2021-03-12T17:15Z)

xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. xmldom versions 0.4.0 and older do not correctly preserve system identifiers, FPIs or namespaces when repeatedly parsing and serializing maliciously crafted documents. This may lead to unexpected syntactic changes during XML processing in some downstream applications. This is fixed in version 0.5.0. As a workaround downstream applications can validate the input and reject the maliciously crafted documents.

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N)

Weakness information:
cwe_id: CWE-436
cwe_name: Interpretation Conflict
description: Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state.
extended_description: This is generally found in proxies, firewalls, anti-virus software, and other intermediary devices that monitor, allow, deny, or modify traffic based on how the client or server is expected to behave.
         
cwe_id: CWE-115
cwe_name: Misinterpretation of Input
description: The product misinterprets an input, whether from an attacker or another product, in a security-relevant fashion.
extended_description: 
            
         
         

Fix information:
repo_url: https://github.com/xmldom/xmldom
commit_details: repo_url: https://github.com/xmldom/xmldom
author: Chris Brody
author_date: 2021-03-08 22:21:56-05:00
msg: Merge pull request from GHSA-h6q6-9hqw-rwfv

* fix!: Preserve quotes in DOCTYPE declaration

Since the only purpose of parsing the DOCTYPE is to be able to restore it when serializing,
we decided that it would be best to leave the parsed publicId and systemId as is,
including any quotes.

BREAKING CHANGE: If somebody relies on the actual unquoted values of those ids,
they will need to take care of either single or double quotes and the right escaping.

(Without this change this would not have been possible because the SAX parser already
dropped the information about the quotes that have been used in the source.)

https://www.w3.org/TR/2006/REC-xml11-20060816/#dtd
https://www.w3.org/TR/2006/REC-xml11-20060816/#IDAX1KS (External Entity Declaration)

Co-authored-by: Christian Bewernitz <coder@karfau.de>
Co-authored-by: Chris Brody <chris.brody+brodybits@gmail.com>

* feat(security): Improve error reporting; throw on duplicate attribute

BREAKING CHANGE: It is currently not clear how to consistently deal with duplicate attributes,
so it is also safer for our users to fail when detecting them.

It is possible to configure the `DOMParser.errorHandler` before parsing, to handle those errors differently.

To accomplish this and also be able to verify it in tests we needed to:

- create a new `Error` type `ParseError` and export it
- Throw `ParseError` from `errorHandler.fatalError` and prevent those from being caught in `XMLReader`.
- export `DOMHandler` constructor as `__DOMHandler`

Co-authored-by: Christian Bewernitz <coder@karfau.de>
Co-authored-by: Chris Brody <chris.brody+brodybits@gmail.com>

Co-authored-by: Christian Bewernitz <coder@karfau.de>
repository: repo_url: https://github.com/xmldom/xmldom
repo_name: xmldom/xmldom
description: A pure JavaScript W3C standard-based (XML DOM Level 2 Core) `DOMParser` and `XMLSerializer` module.
repo_language: HTML
owner: xmldom
file_changes: filename: dom-parser.js
change_type: ModificationType.MODIFY
diff: @@ -178,8 +178,7 @@ DOMHandler.prototype = {
 		console.error('[xmldom error]\t'+error,_locator(this.locator));
 	},
 	fatalError:function(error) {
-		console.error('[xmldom fatalError]\t'+error,_locator(this.locator));
-	    throw error;
+		throw new ParseError(error, this.locator);
 	}
 }
 function _locator(l){
@@ -244,8 +243,11 @@ function appendElement (hander,node) {
 
 //if(typeof require == 'function'){
 var htmlEntity = require('./entities');
-var XMLReader = require('./sax').XMLReader;
+var sax = require('./sax');
+var XMLReader = sax.XMLReader;
+var ParseError = sax.ParseError;
 var DOMImplementation = exports.DOMImplementation = require('./dom').DOMImplementation;
 exports.XMLSerializer = require('./dom').XMLSerializer ;
 exports.DOMParser = DOMParser;
+exports.__DOMHandler = DOMHandler;
 //}

programming_language: JavaScript
filename: dom.js
change_type: ModificationType.MODIFY
diff: @@ -1094,13 +1094,13 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
 		var sysid = node.systemId;
 		buf.push('<!DOCTYPE ',node.name);
 		if(pubid){
-			buf.push(' PUBLIC "',pubid);
+			buf.push(' PUBLIC ', pubid);
 			if (sysid && sysid!='.') {
-				buf.push( '" "',sysid);
+				buf.push(' ', sysid);
 			}
-			buf.push('">');
+			buf.push('>');
 		}else if(sysid && sysid!='.'){
-			buf.push(' SYSTEM "',sysid,'">');
+			buf.push(' SYSTEM ', sysid, '>');
 		}else{
 			var sub = node.internalSubset;
 			if(sub){

programming_language: JavaScript
filename: sax.js
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,21 @@ var S_ATTR_END = 5;//attr value end and no space(quot end)
 var S_TAG_SPACE = 6;//(attr value end || tag end ) && (space offer)
 var S_TAG_CLOSE = 7;//closed el<el />
 
+/**
+ * Creates an error that will not be caught by XMLReader aka the SAX parser.
+ *
+ * @param {string} message
+ * @param {any?} locator Optional, can provide details about the location in the source
+ * @constructor
+ */
+function ParseError(message, locator) {
+	this.message = message
+	this.locator = locator
+	if(Error.captureStackTrace) Error.captureStackTrace(this, ParseError);
+}
+ParseError.prototype = new Error();
+ParseError.prototype.name = ParseError.name
+
 function XMLReader(){
 	
 }
@@ -126,7 +141,7 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){
 						}
 					}
 					if(!endMatch){
-		            	errorHandler.fatalError("end tag name: "+tagName+' is not match the current start tagName:'+config.tagName );
+		            	errorHandler.fatalError("end tag name: "+tagName+' is not match the current start tagName:'+config.tagName ); // No known test case
 					}
 		        }else{
 		        	parseStack.push(config)
@@ -187,10 +202,11 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){
 				}
 			}
 		}catch(e){
+			if (e instanceof ParseError) {
+				throw e;
+			}
 			errorHandler.error('element parse error: '+e)
-			//errorHandler.error('element parse error: '+e);
 			end = -1;
-			//throw e;
 		}
 		if(end>start){
 			start = end;
@@ -211,6 +227,16 @@ function copyLocator(f,t){
  * @return end of the elementStartPart(end of elementEndPart for selfClosed el)
  */
 function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,errorHandler){
+
+	/**
+	 * @param {string} qname
+	 * @param {string} value
+	 * @param {number} startIndex
+	 */
+	function addAttribute(qname, value, startIndex) {
+		if (qname in el.attributeNames) errorHandler.fatalError('Attribute ' + qname + ' redefined')
+		el.addValue(qname, value, startIndex)
+	}
 	var attrName;
 	var value;
 	var p = ++start;
@@ -226,7 +252,7 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 				s = S_EQ;
 			}else{
 				//fatalError: equal must after attrName or space after attrName
-				throw new Error('attribute equal must after attrName');
+				throw new Error('attribute equal must after attrName'); // No known test case
 			}
 			break;
 		case '\'':
@@ -241,7 +267,7 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 				p = source.indexOf(c,start)
 				if(p>0){
 					value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer);
-					el.add(attrName,value,start-1);
+					addAttribute(attrName, value, start-1);
 					s = S_ATTR_END;
 				}else{
 					//fatalError: no end quot match
@@ -250,14 +276,14 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 			}else if(s == S_ATTR_NOQUOT_VALUE){
 				value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer);
 				//console.log(attrName,value,start,p)
-				el.add(attrName,value,start);
+				addAttribute(attrName, value, start);
 				//console.dir(el)
 				errorHandler.warning('attribute "'+attrName+'" missed start quot('+c+')!!');
 				start = p+1;
 				s = S_ATTR_END
 			}else{
 				//fatalError: no equal before
-				throw new Error('attribute value must after "="');
+				throw new Error('attribute value must after "="'); // No known test case
 			}
 			break;
 		case '/':
@@ -275,11 +301,10 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 				break;
 			//case S_EQ:
 			default:
-				throw new Error("attribute invalid close char('/')")
+				throw new Error("attribute invalid close char('/')") // No known test case
 			}
 			break;
 		case ''://end document
-			//throw new Error('unexpected end of input')
 			errorHandler.error('unexpected end of input');
 			if(s == S_TAG){
 				el.setTagName(source.slice(start,p));
@@ -305,13 +330,13 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 					value = attrName;
 				}
 				if(s == S_ATTR_NOQUOT_VALUE){
-					errorHandler.warning('attribute "'+value+'" missed quot(")!!');
-					el.add(attrName,value.replace(/&#?\w+;/g,entityReplacer),start)
+					errorHandler.warning('attribute "'+value+'" missed quot(")!');
+					addAttribute(attrName, value.replace(/&#?\w+;/g,entityReplacer), start)
 				}else{
 					if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !value.match(/^(?:disabled|checked|selected)$/i)){
 						errorHandler.warning('attribute "'+value+'" missed value!! "'+value+'" instead!!')
 					}
-					el.add(value,value,start)
+					addAttribute(value, value, start)
 				}
 				break;
 			case S_EQ:
@@ -336,7 +361,7 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 				case S_ATTR_NOQUOT_VALUE:
 					var value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer);
 					errorHandler.warning('attribute "'+value+'" missed quot(")!!');
-					el.add(attrName,value,start)
+					addAttribute(attrName, value, start)
 				case S_ATTR_END:
 					s = S_TAG_SPACE;
 					break;
@@ -359,7 +384,7 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error
 					if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !attrName.match(/^(?:disabled|checked|selected)$/i)){
 						errorHandler.warning('attribute "'+attrName+'" missed value!! "'+attrName+'" instead2!!')
 					}
-					el.add(attrName,attrName,start);
+					addAttribute(attrName, attrName, start);
 					start = p;
 					s = S_ATTR;
 					break;
@@ -542,8 +567,7 @@ function parseDCC(source,start,domBuilder,errorHandler){//sure start with '<!'
 				}
 			}
 			var lastMatch = matchs[len-1]
-			domBuilder.startDTD(name,pubid && pubid.replace(/^(['"])(.*?)\1$/,'$2'),
-					sysid && sysid.replace(/^(['"])(.*?)\1$/,'$2'));
+			domBuilder.startDTD(name, pubid, sysid);
 			domBuilder.endDTD();
 			
 			return lastMatch.index+lastMatch[0].length
@@ -569,11 +593,8 @@ function parseInstruction(source,start,domBuilder){
 	return -1;
 }
 
-/**
- * @param source
- */
-function ElementAttributes(source){
-	
+function ElementAttributes(){
+	this.attributeNames = {}
 }
 ElementAttributes.prototype = {
 	setTagName:function(tagName){
@@ -582,10 +603,11 @@ ElementAttributes.prototype = {
 		}
 		this.tagName = tagName
 	},
-	add:function(qName,value,offset){
+	addValue:function(qName, value, offset) {
 		if(!tagNamePattern.test(qName)){
 			throw new Error('invalid attribute:'+qName)
 		}
+		this.attributeNames[qName] = this.length;
 		this[this.length++] = {qName:qName,value:value,offset:offset}
 	},
 	length:0,
@@ -621,4 +643,4 @@ function split(source,start){
 }
 
 exports.XMLReader = XMLReader;
-
+exports.ParseError = ParseError;

programming_language: JavaScript
filename: reported-levels.test.js.snap
change_type: ModificationType.ADD
diff: @@ -0,0 +1,501 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value missed!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value missed!!||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value missed!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_AttributeEqualMissingValue with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value missed!!||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_AttributeMissingEndingQuote with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: attribute value no end '\\"' match||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_ElementClosingNotConnected with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to
+@#[line:1,col:6]",
+]
+`;
+
+exports[`SYNTAX_ElementClosingNotConnected with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to||@#[line:1,col:6]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_ElementClosingNotConnected with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to
+@#[line:1,col:6]",
+]
+`;
+
+exports[`SYNTAX_ElementClosingNotConnected with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: elements closed character '/' and '>' must be connected to||@#[line:1,col:6]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	end tag name: inner maybe not complete
+@#[line:1,col:6]",
+]
+`;
+
+exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]
+    at parse (lib/sax.js:128)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]||@#[line:1,col:6]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	end tag name: inner maybe not complete
+@#[line:1,col:6]",
+]
+`;
+
+exports[`SYNTAX_EndTagMaybeNotComplete with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]
+    at parse (lib/sax.js:128)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: inner maybe not complete||@#[line:1,col:6]||@#[line:1,col:6]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_EndTagNotComplete with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	end tag name: xml is not complete:xml
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_EndTagNotComplete with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]
+    at parse (lib/sax.js:124)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_EndTagNotComplete with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	end tag name: xml is not complete:xml
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_EndTagNotComplete with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]
+    at parse (lib/sax.js:124)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	end tag name: xml is not complete:xml||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_InvalidAttributeName with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid attribute:123
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_InvalidAttributeName with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid attribute:123||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_InvalidAttributeName with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid attribute:123
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_InvalidAttributeName with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid attribute:123||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_InvalidTagName with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid tagName:123
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_InvalidTagName with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid tagName:123||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_InvalidTagName with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid tagName:123
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_InvalidTagName with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	element parse error: Error: invalid tagName:123||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_UnclosedComment with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	Unclosed comment
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_UnclosedComment with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	Unclosed comment||@#[line:1,col:1]
+    at parseDCC (lib/sax.js:538)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	Unclosed comment||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_UnclosedComment with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	Unclosed comment
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_UnclosedComment with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	Unclosed comment||@#[line:1,col:1]
+    at parseDCC (lib/sax.js:538)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	Unclosed comment||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	unexpected end of input
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	unexpected end of input||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:308)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	unexpected end of input||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	unexpected end of input
+@#[line:1,col:1]",
+]
+`;
+
+exports[`SYNTAX_UnexpectedEndOfInput with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	unexpected end of input||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:308)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	unexpected end of input||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`WF_AttributeMissingQuote with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingQuote with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:333)",
+]
+`;
+
+exports[`WF_AttributeMissingQuote with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingQuote with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"value\\" missed quot(\\")!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:333)",
+]
+`;
+
+exports[`WF_AttributeMissingQuote2 with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingQuote2 with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:363)",
+]
+`;
+
+exports[`WF_AttributeMissingQuote2 with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingQuote2 with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:363)",
+]
+`;
+
+exports[`WF_AttributeMissingStartingQuote with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingStartingQuote with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:281)",
+]
+`;
+
+exports[`WF_AttributeMissingStartingQuote with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingStartingQuote with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed start quot(\\")!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:281)",
+]
+`;
+
+exports[`WF_AttributeMissingValue with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingValue with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:337)",
+]
+`;
+
+exports[`WF_AttributeMissingValue with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingValue with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:337)",
+]
+`;
+
+exports[`WF_AttributeMissingValue2 with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!
+@#[line:1,col:1]",
+  "[xmldom warning]	attribute \\"attr2\\" missed value!! \\"attr2\\" instead!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingValue2 with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:385)",
+]
+`;
+
+exports[`WF_AttributeMissingValue2 with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!
+@#[line:1,col:1]",
+  "[xmldom warning]	attribute \\"attr2\\" missed value!! \\"attr2\\" instead!!
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeMissingValue2 with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute \\"attr\\" missed value!! \\"attr\\" instead2!!||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:385)",
+]
+`;
+
+exports[`WF_AttributeValueMustAfterEqual with mimeType text/html should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute value must after \\"=\\"
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeValueMustAfterEqual with mimeType text/html should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute value must after \\"=\\"||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:263)",
+]
+`;
+
+exports[`WF_AttributeValueMustAfterEqual with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	attribute value must after \\"=\\"
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_AttributeValueMustAfterEqual with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	attribute value must after \\"=\\"||@#[line:1,col:1]
+    at parseElementStartPart (lib/sax.js:263)",
+]
+`;
+
+exports[`WF_DuplicateAttribute with mimeType text/html should be reported as fatalError 1`] = `
+Array [
+  "[xmldom fatalError]	Attribute a redefined
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_DuplicateAttribute with mimeType text/xml should be reported as fatalError 1`] = `
+Array [
+  "[xmldom fatalError]	Attribute a redefined
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_EntityDeclared with mimeType text/html should be reported as error 1`] = `
+Array [
+  "[xmldom error]	entity not found:&e;
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_EntityDeclared with mimeType text/html should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	entity not found:&e;||@#[line:1,col:1]
+    at replace (lib/sax.js:71)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	entity not found:&e;||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`WF_EntityDeclared with mimeType text/xml should be reported as error 1`] = `
+Array [
+  "[xmldom error]	entity not found:&e;
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_EntityDeclared with mimeType text/xml should not catch Error thrown in errorHandler.error 1`] = `
+Array [
+  "[xmldom error]	entity not found:&e;||@#[line:1,col:1]
+    at replace (lib/sax.js:71)",
+  "[xmldom error]	element parse error: Error: [xmldom error]	entity not found:&e;||@#[line:1,col:1]||@#[line:1,col:1]
+    at parse (lib/sax.js:208)",
+]
+`;
+
+exports[`WF_UnclosedXmlAttribute with mimeType text/xml should be reported as warning 1`] = `
+Array [
+  "[xmldom warning]	unclosed xml attribute
+@#[line:1,col:1]",
+]
+`;
+
+exports[`WF_UnclosedXmlAttribute with mimeType text/xml should escalate Error thrown in errorHandler.warning to errorHandler.error 1`] = `
+Array [
+  "[xmldom warning]	unclosed xml attribute||@#[line:1,col:1]
+    at parse (lib/sax.js:173)",
+]
+`;

programming_language: CoffeeScript
filename: xml-error.test.js.snap
change_type: ModificationType.MODIFY
diff: @@ -4,7 +4,7 @@ exports[`html vs xml: html attribute (miss quote) 1`] = `
 Object {
   "actual": "<img attr=\\"1\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"/>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
@@ -37,21 +37,6 @@ Object {
   ],
 }
 `;
-exports[`html vs xml: unclosed document in text/xml 1`] = `
-Object {
-  "actual": "<img/>",
-  "warning": Array [
-    "[xmldom warning]	unclosed xml attribute
-@#[line:1,col:1]",
-  ],
-}
-`;
-
-exports[`html vs xml: unclosed document in text/html 1`] = `
-Object {
-  "actual": "<img xmlns=\\"http://www.w3.org/1999/xhtml\\"/>",
-}
-`;
 
 exports[`html vs xml: text/html attribute (missing =) 1`] = `
 Object {
@@ -94,3 +79,19 @@ Object {
   ],
 }
 `;
+
+exports[`html vs xml: unclosed document in text/html 1`] = `
+Object {
+  "actual": "<img xmlns=\\"http://www.w3.org/1999/xhtml\\"/>",
+}
+`;
+
+exports[`html vs xml: unclosed document in text/xml 1`] = `
+Object {
+  "actual": "<img/>",
+  "warning": Array [
+    "[xmldom warning]	unclosed xml attribute
+@#[line:1,col:1]",
+  ],
+}
+`;

programming_language: TypeScript
filename: error-handler.test.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,51 @@
+'use strict'
+
+const { DOMParser } = require('../../lib/dom-parser')
+const { REPORTED } = require('./reported')
+
+describe('custom errorHandler', () => {
+	it('function with two args receives key and message', () => {
+		const errors = {}
+		const parser = new DOMParser({
+			// currently needs to be a `function` to make the test work,
+			// `jest.fn()` or using `() => {}` doesn't work
+			errorHandler: function (key, msg) {
+				errors[key] = msg
+			},
+		})
+
+		parser.parseFromString(REPORTED.WF_AttributeMissingQuote.source, 'text/xml')
+		expect(errors).toHaveProperty('warning')
+		parser.parseFromString(
+			REPORTED.SYNTAX_AttributeEqualMissingValue.source,
+			'text/xml'
+		)
+		expect(errors).toHaveProperty('error')
+		parser.parseFromString(REPORTED.WF_DuplicateAttribute.source, 'text/xml')
+		expect(errors).toHaveProperty('fatalError')
+	})
+
+	it('function with one argument builds list', () => {
+		const errors = []
+		const parser = new DOMParser({
+			// currently needs to be a `function` to make the test work,
+			// `jest.fn()` or using `() => {}` doesn't work
+			errorHandler: function (msg) {
+				errors.push(msg)
+			},
+		})
+
+		parser.parseFromString(REPORTED.WF_AttributeMissingQuote.source, 'text/xml')
+		parser.parseFromString(
+			REPORTED.SYNTAX_AttributeEqualMissingValue.source,
+			'text/xml'
+		)
+		parser.parseFromString(REPORTED.WF_DuplicateAttribute.source, 'text/xml')
+
+		expect(errors).toMatchObject([
+			/\[xmldom warning]/,
+			/\[xmldom error]/,
+			/\[xmldom fatalError]/,
+		])
+	})
+})

programming_language: JavaScript
filename: error.test.js
change_type: ModificationType.DELETE
diff: @@ -1,83 +0,0 @@
-'use strict'
-
-const { DOMParser } = require('../../lib/dom-parser')
-
-const XML_ERROR = '<html><body title="1<2"><table&lt;;test</body></body></html>'
-const XML_ERROR_AND_WARNING = '<html disabled><1 1="2"/></body></html>'
-
-describe('errorHandler', () => {
-	it('only single function with two args builds map', () => {
-		const errors = {}
-		const parser = new DOMParser({
-			errorHandler: function (key, msg) {
-				errors[key] = msg
-			},
-		})
-
-		parser.parseFromString(XML_ERROR_AND_WARNING, 'text/xml')
-
-		expect(errors).toMatchObject({
-			warning: expect.stringMatching(/.*/),
-			error: expect.stringMatching(/.*/),
-		})
-	})
-
-	it('only one function with one argument builds list', () => {
-		const errors = []
-		const parser = new DOMParser({
-			errorHandler: function (msg) {
-				errors.push(msg)
-			},
-		})
-
-		parser.parseFromString(XML_ERROR_AND_WARNING, 'text/xml')
-
-		expect(errors).toMatchObject([/\[xmldom warning]/, /\[xmldom error]/])
-	})
-
-	it.each(['warning', 'error'])(
-		'errorHandler for only one level: %s',
-		(level) => {
-			const errors = []
-			const parser = new DOMParser({
-				errorHandler: {
-					[level]: function (msg) {
-						errors.push(msg)
-					},
-				},
-			})
-
-			parser.parseFromString(XML_ERROR_AND_WARNING, 'text/xml')
-
-			expect(errors).toHaveLength(1)
-		}
-	)
-	it.todo(
-		'errorHandler for only one level: fatalError'
-		/*
-		I was not able to create a test case for a fatalError.
-		It might need to be removed from the API, since all but one cases are in comments
-		and an error is thrown instead. The one case left I was not able to reproduce.
-		*/
-	)
-
-	it('error function throwing is not caught', () => {
-		const errors = []
-		const ERROR_MSG = 'FROM TEST'
-
-		const parser = new DOMParser({
-			locator: {}, // removing the locator makes the test fail!
-			errorHandler: {
-				error: function (msg) {
-					errors.push(msg)
-					throw new Error(ERROR_MSG)
-				},
-			},
-		})
-
-		expect(() => {
-			parser.parseFromString(XML_ERROR, 'text/html')
-		}).toThrow(ERROR_MSG)
-		expect(errors).toMatchObject([/\n@#\[line\:\d+,col\:\d+\]/])
-	})
-})

programming_language: unknown
filename: reported-levels.test.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,106 @@
+'use strict'
+
+const { REPORTED } = require('./reported')
+const { getTestParser } = require('../get-test-parser')
+const { ParseError } = require('../../lib/sax')
+const { DOMParser } = require('../../lib/dom-parser')
+
+describe.each(Object.entries(REPORTED))(
+	'%s',
+	(name, { source, level, match, skippedInHtml }) => {
+		describe.each(['text/xml', 'text/html'])('with mimeType %s', (mimeType) => {
+			const isHtml = mimeType === 'text/html'
+			if (isHtml && skippedInHtml) {
+				it(`should not be reported as ${level}`, () => {
+					const { errors, parser } = getTestParser()
+
+					parser.parseFromString(source, mimeType)
+
+					// if no report was triggered, the key is not present on `errors`
+					expect(errors[level]).toBeUndefined()
+				})
+			} else {
+				it(`should be reported as ${level}`, () => {
+					const { errors, parser } = getTestParser()
+
+					parser.parseFromString(source, mimeType)
+
+					const reported = errors[level]
+					// store the snapshot, so any change in message can be inspected in the git diff
+					expect(reported).toMatchSnapshot()
+					// if a match has been defined, filter messages
+					expect(
+						match ? (reported || []).filter(match) : reported
+					).toHaveLength(1)
+				})
+				if (level === 'fatalError') {
+					it(`should throw ParseError in errorHandler.fatalError`, () => {
+						const parser = new DOMParser()
+
+						expect(() => parser.parseFromString(source, mimeType)).toThrow(
+							ParseError
+						)
+					})
+				} else if (level === 'error') {
+					it(`should not catch Error thrown in errorHandler.${level}`, () => {
+						let thrown = []
+						const errorHandler = {
+							[level]: jest.fn((message) => {
+								const toThrow = new Error(message)
+								thrown.push(toThrow)
+								throw toThrow
+							}),
+						}
+						const { parser } = getTestParser({ errorHandler })
+
+						expect(() => parser.parseFromString(source, mimeType)).toThrow(
+							Error
+						)
+						expect(thrown.map(toErrorSnapshot)).toMatchSnapshot()
+						match && expect(match(thrown[0].toString())).toBe(true)
+					})
+				} else if (level === 'warning') {
+					it('should escalate Error thrown in errorHandler.warning to errorHandler.error', () => {
+						let thrown = []
+						const errorHandler = {
+							warning: jest.fn((message) => {
+								const toThrow = new Error(message)
+								thrown.push(toThrow)
+								throw toThrow
+							}),
+							error: jest.fn(),
+						}
+						const { parser } = getTestParser({ errorHandler })
+
+						parser.parseFromString(source, mimeType)
+
+						expect(errorHandler.warning).toHaveBeenCalledTimes(1)
+						expect(errorHandler.error).toHaveBeenCalledTimes(1)
+						expect(thrown.map(toErrorSnapshot)).toMatchSnapshot()
+						match && expect(match(thrown[0].message)).toBe(true)
+					})
+				}
+			}
+		})
+	}
+)
+
+/**
+ * Creates a string from an error that is easily readable in a snapshot
+ * - put's the message on one line as first line
+ * - picks the first line in the stack trace that is in `lib/sax.js`,
+ *   and strips absolute paths and character position from that stack entry
+ *   as second line
+ * @param {Error} error
+ */
+function toErrorSnapshot(error) {
+	const libSaxMatch = /\/.*\/(lib\/sax\.js)/
+	return `${error.message.replace(/([\n\r]+\s*)/g, '||')}\n${error.stack
+		.split(/[\n\r]+/)
+		// find first line that is from lib/sax.js
+		.filter((l) => libSaxMatch.test(l))[0]
+		// strip of absolute path
+		.replace(libSaxMatch, '$1')
+		// strip of position of character in line
+		.replace(/:\d+\)$/, ')')}`
+}

programming_language: JavaScript
filename: reported.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,243 @@
+'use strict'
+
+/**
+ * @typedef ErrorReport
+ * @property {string} source the XML snippet
+ * @property {'error'|'warning'|'fatalError'} level the name of the method triggered
+ * @property {?function(msg:string):boolean} match to pick the relevant report when there are multiple
+ * @property {?boolean} skippedInHtml Is the error reported when parsing HTML?
+ */
+/**
+ * A collection of XML samples and related information that cause the XMLReader
+ * to call methods on `errorHandler`.
+ *
+ * @type {Record<string, ErrorReport>}}
+ */
+const REPORTED = {
+	/**
+	 * Entities need to be in the entityMap to be converted as part of parsing.
+	 * xmldom currently doesn't parse entities declared in DTD.
+	 *
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#wf-entdeclared
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#wf-entdeclared
+	 */
+	WF_EntityDeclared: {
+		source: '<xml>&e;</xml>',
+		level: 'error',
+		match: (msg) => /entity not found/.test(msg),
+	},
+	/**
+	 * Well-formedness constraint: Unique Att Spec
+	 *
+	 * An attribute name must not appear more than once
+	 * in the same start-tag or empty-element tag.
+	 *
+	 * In the browser:
+	 * - as XML it is reported as `error on line 1 at column 17: Attribute a redefined`
+	 * - as HTML only the first definition is considered
+	 *
+	 * In xmldom the behavior is different for namespaces (picks first)
+	 * than for other attributes (picks last),
+	 * which can be a security issue.
+	 *
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#uniqattspec
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#uniqattspec
+	 */
+	WF_DuplicateAttribute: {
+		source: '<xml a="1" a="2"></xml>',
+		level: 'fatalError',
+		match: (msg) => /Attribute .* redefined/.test(msg),
+	},
+	/**
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser it is reported as `error on line 1 at column 14: expected '>'`,
+	 * but still adds the root level element to the dom.
+	 */
+	SYNTAX_EndTagNotComplete: {
+		source: '<xml></xml',
+		level: 'error',
+		match: (msg) => /end tag name/.test(msg) && /is not complete/.test(msg),
+	},
+	/**
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser it is reported as `error on line 1 at column 21: expected '>'`,
+	 * but still adds the root level element and inner tag to the dom.
+	 */
+	SYNTAX_EndTagMaybeNotComplete: {
+		source: '<xml><inner></inner </xml>',
+		level: 'error',
+		match: (msg) => /end tag name/.test(msg) && /maybe not complete/.test(msg),
+	},
+	/**
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser it is reported as `error on line 1 at column 6: Comment not terminated`.
+	 */
+	SYNTAX_UnclosedComment: {
+		source: '<!--',
+		level: 'error',
+		match: (msg) => /Unclosed comment/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:596, caught in 208
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser:
+	 * - as XML it is reported as
+	 * `error on line 1 at column 2: StartTag: invalid element name`
+	 * - as HTML it is accepted as characters
+	 *
+	 */
+	SYNTAX_InvalidTagName: {
+		source: '<123 />',
+		level: 'error',
+		match: (msg) => /invalid tagName/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:602, caught in 208
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser:
+	 * - as XML it is reported as
+	 * `error on line 1 at column 6: error parsing attribute name`
+	 * - as HTML it is accepted as attribute name
+	 */
+	SYNTAX_InvalidAttributeName: {
+		source: '<xml 123=""/>',
+		level: 'error',
+		match: (msg) => /invalid attribute/.test(msg),
+	},
+	/**
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser it is reported as `error on line 1 at column 5: Couldn't find end of Start Tag xml`.
+	 */
+	SYNTAX_UnexpectedEndOfInput: {
+		source: '<xml',
+		level: 'error',
+		match: (msg) => /unexpected end of input/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:392, caught in 208
+	 * This sample doesn't follow the specified grammar.
+	 * In the browser:
+	 * - in XML it is reported as `error on line 1 at column 8: error parsing attribute name`
+	 * - in HTML it produces `<xml><a <="" xml=""></a></xml>` (invalid XML?)
+	 */
+	SYNTAX_ElementClosingNotConnected: {
+		source: '<xml><a/ </xml>',
+		level: 'error',
+		match: (msg) => /must be connected/.test(msg),
+	},
+	/**
+	 * In the Browser (for XML) this is reported as
+	 * `error on line 1 at column 6: Extra content at the end of the document`
+	 * for HTML it's added to the DOM without anything being reported.
+	 */
+	WF_UnclosedXmlAttribute: {
+		source: '<xml>',
+		level: 'warning',
+		skippedInHtml: true,
+		match: (msg) => /unclosed xml attribute/.test(msg),
+	},
+	/**
+	 * In the browser:
+	 * - for XML it is reported as
+	 * `error on line 1 at column 10: Specification mandates value for attribute attr`
+	 * - for HTML is uses the attribute as one with no value and adds `"value"` to the attribute name
+	 *   and is not reporting any issue.
+	 */
+	WF_AttributeValueMustAfterEqual: {
+		source: '<xml attr"value" />',
+		level: 'warning',
+		match: (msg) => /attribute value must after "="/.test(msg),
+	},
+	/**
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 11: AttValue: " or ' expected`
+	 * - for HTML is wraps `value"` with quotes and is not reporting any issue.
+	 */
+	WF_AttributeMissingStartingQuote: {
+		source: '<xml attr=value" />',
+		level: 'warning',
+		match: (msg) => /missed start quot/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:264, caught in 208.
+	 * TODO: Comment indicates fatalError, change to use errorHandler.fatalError?
+	 *
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 20: AttValue: ' expected`
+	 * - for HTML nothing is added to the DOM.
+	 */
+	SYNTAX_AttributeMissingEndingQuote: {
+		source: '<xml attr="value />',
+		level: 'error',
+		match: (msg) => /attribute value no end .* match/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:324
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 11: AttValue: " or ' expected`
+	 * - for HTML is wraps `value/` with quotes and is not reporting any issue.
+	 */
+	WF_AttributeMissingQuote: {
+		source: '<xml attr=value/>',
+		level: 'warning',
+		match: (msg) => / missed quot/.test(msg) && /!!/.test(msg) === false,
+	},
+	/**
+	 * Triggered by lib/sax.js:354
+	 * This is the only warning reported in this sample.
+	 * For some reason the "attribute" that is reported as missing quotes
+	 * has the name `&`.
+	 * This case is also present in 2 tests in test/html/normalize.test.js
+	 *
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 8: AttValue: " or ' expected`
+	 * - for HTML is yields `<xml a="&amp;" b="&amp;"></xml>` and is not reporting any issue.
+	 */
+	WF_AttributeMissingQuote2: {
+		source: `<xml a=& b="&"/>`,
+		level: 'warning',
+		match: (msg) => / missed quot/.test(msg) && /!!/.test(msg),
+	},
+	/**
+	 * In the browser:
+	 * - for XML it is reported as `error on line 1 at column 9: AttValue: " or ' expected`
+	 * - for HTML is yields `<doc a1></xml>` and is not reporting any issue.
+	 *
+	 * But the XML specifications does not allow that:
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Attribute
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Attribute
+	 */
+	SYNTAX_AttributeEqualMissingValue: {
+		source: '<doc a1=></doc>',
+		level: 'error',
+		match: (msg) => /attribute value missed!!/.test(msg),
+	},
+	/**
+	 * In the browser this is not an issue at all, but just add an attribute without a value.
+	 * But the XML specifications does not allow that:
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Attribute
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Attribute
+	 */
+	WF_AttributeMissingValue: {
+		source: '<xml attr ></xml>',
+		level: 'warning',
+		match: (msg) => /missed value/.test(msg) && /instead!!/.test(msg),
+	},
+	/**
+	 * Triggered by lib/sax.js:376
+	 * This seems to only be reached when there are two subsequent attributes with a missing value
+	 * In the browser this is not an issue at all, but just add an attribute without a value.
+	 * But the XML specifications does not allow that:
+	 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Attribute
+	 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Attribute
+	 */
+	WF_AttributeMissingValue2: {
+		source: '<xml attr attr2 ></xml>',
+		level: 'warning',
+		match: (msg) => /missed value/.test(msg) && /instead2!!/.test(msg),
+	},
+}
+
+module.exports = {
+	REPORTED,
+}

programming_language: TypeScript
filename: xml-reader-dom-handler-errors.test.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,140 @@
+'use strict'
+
+const { ParseError } = require('../../lib/sax')
+const { __DOMHandler, DOMParser } = require('../../lib/dom-parser')
+
+/**
+ * All methods implemented on the DOMHandler prototype.
+ *
+ * @type {string[]}
+ */
+const DOMHandlerMethods = Object.keys(__DOMHandler.prototype).sort()
+
+/**
+ * XMLReader is currently not calling all methods "implemented" by DOMHandler (some are just empty),
+ * If this changes the first test will fail.
+ *
+ * @type {Set<string>}
+ */
+const UNCALLED_METHODS = new Set([
+	'attributeDecl',
+	'elementDecl',
+	'endEntity',
+	'externalEntityDecl',
+	'getExternalSubset',
+	'ignorableWhitespace',
+	'internalEntityDecl',
+	'notationDecl',
+	'resolveEntity',
+	'skippedEntity',
+	'startEntity',
+	'unparsedEntityDecl',
+])
+
+/**
+ * Some of the methods DOMParser/XMLReader call during parsing are not guarded by try/catch,
+ * hence an error happening in those will stop the parsing process.
+ * There is a test to verify this error handling.
+ * If it changes this list might need to be changed as well
+ *
+ * @type {Set<string>}
+ */
+const UNCAUGHT_METHODS = new Set([
+	'characters',
+	'endDocument',
+	'error',
+	'setDocumentLocator',
+	'startDocument',
+])
+
+function noop() {}
+
+/**
+ * A subclass of DOMHandler that mocks all methods for later inspection.
+ * As part of the constructor it can be told which method is supposed to throw an error
+ * and which error constructor to use.
+ *
+ * The `methods` property provides the list of all mocks.
+ */
+class StubDOMHandler extends __DOMHandler {
+	constructor(throwingMethod, ErrorClass) {
+		super()
+		this.methods = []
+		DOMHandlerMethods.forEach((method) => {
+			const impl = jest.fn(
+				method === throwingMethod
+					? () => {
+							throw new (ErrorClass || ParseError)(
+								`StubDOMHandler throwing in ${throwingMethod}`
+							)
+					  }
+					: noop
+			)
+			impl.mockName(method)
+			this[method] = impl
+			this.methods.push(impl)
+		})
+	}
+}
+/**
+ * This sample is triggering all method calls from XMLReader to DOMHandler at least once.
+ * This is verified in a test.
+ *
+ * There is of course no guarantee that it triggers all the places where XMLReader calls DOMHandler.
+ * For example not all possible warning and error cases are present in this file,
+ * but some, so that the methods are triggered.
+ * For testing all the cases of the different error levels,
+ * there are samples per case in
+ * @see REPORTED
+ */
+const ALL_METHODS = `<?xml ?>
+<!DOCTYPE name >
+<![CDATA[ raw ]]>
+<root xmlns="namespace">
+  <!-- -->
+  <element xmlns:x="http://test" x:a="" warning>
+    character
+  </element>
+  <element duplicate="" duplicate="fatal"></mismatch>
+</root>
+<!--
+`
+
+describe('methods called in DOMHandler', () => {
+	it('should call "all possible" methods when using StubDOMHandler', () => {
+		const domBuilder = new StubDOMHandler()
+		const parser = new DOMParser({ domBuilder, locator: {} })
+		expect(domBuilder.methods).toHaveLength(DOMHandlerMethods.length)
+
+		parser.parseFromString(ALL_METHODS)
+
+		const uncalledMethodNames = domBuilder.methods
+			.filter((m) => m.mock.calls.length === 0)
+			.map((m) => m.getMockName())
+		expect(uncalledMethodNames).toEqual([...UNCALLED_METHODS.values()].sort())
+	})
+	describe.each(DOMHandlerMethods.filter((m) => !UNCALLED_METHODS.has(m)))(
+		'when DOMHandler.%s throws',
+		(throwing) => {
+			it('should not catch ParserError', () => {
+				const domBuilder = new StubDOMHandler(throwing, ParseError)
+				const parser = new DOMParser({ domBuilder, locator: {} })
+
+				expect(() => parser.parseFromString(ALL_METHODS)).toThrow(ParseError)
+			})
+			const isUncaughtMethod = UNCAUGHT_METHODS.has(throwing)
+			it(`${
+				isUncaughtMethod ? 'does not' : 'should'
+			} catch other Error`, () => {
+				const domBuilder = new StubDOMHandler(throwing, Error)
+				const parser = new DOMParser({ domBuilder, locator: {} })
+
+				if (isUncaughtMethod) {
+					expect(() => parser.parseFromString(ALL_METHODS)).toThrow()
+				} else {
+					expect(() => parser.parseFromString(ALL_METHODS)).not.toThrow()
+				}
+			})
+		}
+	)
+})

programming_language: TypeScript
filename: normalize.test.js.snap
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,10 @@ Object {
 exports[`html normalizer <div a=& a="&''" b/> 1`] = `
 Object {
   "actual": "<div a=\\"&amp;''\\" b=\\"b\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"></div>",
+  "fatalError": Array [
+    "[xmldom fatalError]	Attribute a redefined
+@#[line:1,col:1]",
+  ],
   "warning": Array [
     "[xmldom warning]	attribute \\"&\\" missed quot(\\")!!
 @#[line:1,col:1]",
@@ -32,7 +36,7 @@ Object {
 @#[line:1,col:1]",
     "[xmldom warning]	attribute \\"c\\" missed value!! \\"c\\" instead2!!
 @#[line:1,col:1]",
-    "[xmldom warning]	attribute \\"123&&456\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"123&&456\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
@@ -178,7 +182,7 @@ exports[`html normalizer unclosed html <html title = 1/> 1`] = `
 Object {
   "actual": "<html title=\\"1\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"></html>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
@@ -188,7 +192,7 @@ exports[`html normalizer unclosed html <html title =1/2></html> 1`] = `
 Object {
   "actual": "<html title=\\"1/2\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"></html>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"1/2\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"1/2\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
@@ -208,7 +212,7 @@ exports[`html normalizer unclosed html <html title= 1/> 1`] = `
 Object {
   "actual": "<html title=\\"1\\" xmlns=\\"http://www.w3.org/1999/xhtml\\"></html>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"1\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }

programming_language: HTML
filename: doctype.test.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,27 @@
+'use strict'
+
+const { getTestParser } = require('../get-test-parser')
+describe('doctype', () => {
+	describe.each(['SYSTEM', 'PUBLIC'])('%s', (idType) => {
+		test.each([
+			['outer single', `<!DOCTYPE x ${idType} '\"'><X/>`, "'\"'"],
+			['outer double', `<!DOCTYPE x ${idType} "\'"><X/>`, '"\'"'],
+		])(
+			'should parse single line DOCTYPE with mixed quotes (%s)',
+			(_, source, idValue) => {
+				const { errors, parser } = getTestParser()
+
+				const actual = parser.parseFromString(source).firstChild
+
+				expect({
+					[idType]: idType === 'SYSTEM' ? actual.systemId : actual.publicId,
+					name: actual.name,
+					...errors,
+				}).toEqual({
+					[idType]: idValue,
+					name: 'x',
+				})
+			}
+		)
+	})
+})

programming_language: JavaScript
filename: parse-error.test.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,38 @@
+'use strict'
+const { ParseError } = require('../../lib/sax')
+
+describe('ParseError', () => {
+	it('should have name ParseError', () => {
+		expect(new ParseError('').name).toBe('ParseError')
+	})
+	it('should be an instance of Error', () => {
+		expect(new ParseError('') instanceof Error).toBe(true)
+	})
+
+	it('should be an instance of ParseError', () => {
+		expect(new ParseError('') instanceof ParseError).toBe(true)
+	})
+
+	it('should store first argument as message', () => {
+		const error = new ParseError('FROM TEST')
+		expect(error.message).toBe('FROM TEST')
+	})
+
+	it('should store second argument as locator', () => {
+		const locator = {}
+		const error = new ParseError('', locator)
+		expect(error.locator).toBe(locator)
+	})
+
+	it('should have correct StackTrace', () => {
+		const error = new ParseError('MESSAGE')
+		const stack = error.stack && error.stack.split(/[\n\r]+/)
+		expect(stack && stack.length).toBeGreaterThan(1)
+		expect(stack[0]).toBe('ParseError: MESSAGE')
+		expect(stack[1]).toContain(__filename)
+	})
+
+	it('Error should not be instanceof ParseError', () => {
+		expect(new Error() instanceof ParseError).toBe(false)
+	})
+})

programming_language: JavaScript
filename: not-wf.test.js.snap
change_type: ModificationType.MODIFY
diff: @@ -94,7 +94,7 @@ exports[`xmltest/not-wellformed standalone should match 012.xml with snapshot 1`
 Object {
   "actual": "<doc a1=\\"v1\\"/>",
   "warning": Array [
-    "[xmldom warning]	attribute \\"v1\\" missed quot(\\")!!
+    "[xmldom warning]	attribute \\"v1\\" missed quot(\\")!
 @#[line:1,col:1]",
   ],
 }
@@ -300,6 +300,10 @@ Object {
 exports[`xmltest/not-wellformed standalone should match 038.xml with snapshot 1`] = `
 Object {
   "actual": "<doc x=\\"baz\\" y=\\"bar\\"/>",
+  "fatalError": Array [
+    "[xmldom fatalError]	Attribute x redefined
+@#[line:1,col:1]",
+  ],
 }
 `;
 

programming_language: TypeScript