diff --git a/spec/validator_spec.js b/spec/validator_spec.js
index 0d07e227..4d25dab9 100644
--- a/spec/validator_spec.js
+++ b/spec/validator_spec.js
@@ -475,4 +475,26 @@ describe("XML Validator with options", function () {
expect(result).toBeTrue();
});
-});
\ No newline at end of file
+ it('- literal & not starting a reference in attribute value', () =>
+ validate(``,
+ {InvalidAttr: "Attribute 'attr' has an invalid value: value contains '&' that doesn't start a reference."}, 1, 9));
+
+ it('- literal < in attribute value', () =>
+ validate(``,
+ {InvalidAttr: "Attribute 'attr' has an invalid value: value can't contain '<'."}, 1, 9));
+
+ it('- & without a name after it in attribute value', () =>
+ validate(``,
+ {InvalidAttr: "Attribute 'attr' has an invalid value: value contains '&' that doesn't start a reference."}, 1, 9));
+
+ it('- escaped references like < and & in attribute value stay valid', () => {
+ expect(XMLValidator.validate(``)).toBeTrue();
+ expect(XMLValidator.validate(``)).toBeTrue();
+ expect(XMLValidator.validate(``)).toBeTrue();
+ expect(XMLValidator.validate(``)).toBeTrue();
+ expect(XMLValidator.validate(``)).toBeTrue();
+ });
+
+ it('- HTML-style legacy reference in attribute value stays valid', () =>
+ expect(XMLValidator.validate(``)).toBeTrue());
+});
diff --git a/src/validator.js b/src/validator.js
index 6f9904f0..47a95c8c 100644
--- a/src/validator.js
+++ b/src/validator.js
@@ -409,6 +409,29 @@ function scanAttributeTokens(attrStr) {
//attr, ="sd", a="amit's", a="sd"b="saf", ab cd=""
+
+/**
+ * Check an attribute value for characters that can never appear literally:
+ * a '<', or a '&' that is not followed by a character which can begin a
+ * character or entity reference ('#' or a name start character). The
+ * reference itself is not validated: the parser intentionally accepts
+ * HTML-style legacy references such as "&bar", so name-like runs after
+ * '&' stay accepted here as well.
+ */
+function validateAttributeValue(value) {
+ for (let i = 0; i < value.length; i++) {
+ const ch = value[i];
+ if (ch === '<') {
+ return "value can't contain '<'";
+ } else if (ch === '&') {
+ const next = value[i + 1];
+ if (next === undefined || !(next === '#' || /[A-Za-z_:]/.test(next))) {
+ return "value contains '&' that doesn't start a reference";
+ }
+ }
+ }
+}
+
function validateAttributeString(attrStr, options) {
//console.log("start:"+attrStr+":end");
@@ -440,6 +463,13 @@ function validateAttributeString(attrStr, options) {
} else {
return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' is repeated.", getPositionFromMatch(matches[i]));
}
+ const attrValue = matches[i][6];
+ if (attrValue !== undefined) {
+ const valueError = validateAttributeValue(attrValue);
+ if (valueError) {
+ return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' has an invalid value: " + valueError + ".", getPositionFromMatch(matches[i]));
+ }
+ }
}
return true;
@@ -515,4 +545,4 @@ function getLineNumberForPosition(xmlData, index) {
//this function returns the position of the first character of match within attrStr
function getPositionFromMatch(match) {
return match.startIndex + match[1].length;
-}
\ No newline at end of file
+}