From 433616b9198d03ab7ab00e8c68ab0086fd168d22 Mon Sep 17 00:00:00 2001 From: ANTO JEROME A Date: Wed, 4 Nov 2026 08:02:35 +0530 Subject: [PATCH 1/2] fix: reject literal '<' and stray '&' in attribute values --- src/validator.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) 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 +} From 8099fc0d6d6b4aa1f0a77cad5d397a0fb94a1656 Mon Sep 17 00:00:00 2001 From: ANTO JEROME A Date: Wed, 4 Nov 2026 08:03:00 +0530 Subject: [PATCH 2/2] test: cover literal '<' and stray '&' in attribute values --- spec/validator_spec.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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()); +});