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 +}