Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion spec/validator_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,4 +475,26 @@ describe("XML Validator with options", function () {
expect(result).toBeTrue();
});

});
it('- literal & not starting a reference in attribute value', () =>
validate(`<parent attr="a & b"></parent>`,
{InvalidAttr: "Attribute 'attr' has an invalid value: value contains '&' that doesn't start a reference."}, 1, 9));

it('- literal < in attribute value', () =>
validate(`<parent attr="a < b"></parent>`,
{InvalidAttr: "Attribute 'attr' has an invalid value: value can't contain '<'."}, 1, 9));

it('- & without a name after it in attribute value', () =>
validate(`<parent attr="a &; b"></parent>`,
{InvalidAttr: "Attribute 'attr' has an invalid value: value contains '&' that doesn't start a reference."}, 1, 9));

it('- escaped references like &lt; and &amp; in attribute value stay valid', () => {
expect(XMLValidator.validate(`<parent attr="a &amp; b"></parent>`)).toBeTrue();
expect(XMLValidator.validate(`<parent attr="a &#65; b"></parent>`)).toBeTrue();
expect(XMLValidator.validate(`<parent attr="a &#x41; b"></parent>`)).toBeTrue();
expect(XMLValidator.validate(`<parent attr="a &lt; b"></parent>`)).toBeTrue();
expect(XMLValidator.validate(`<parent attr="url?a=1&amp;b=2"></parent>`)).toBeTrue();
});

it('- HTML-style legacy reference in attribute value stays valid', () =>
expect(XMLValidator.validate(`<parent attr="2foo&ampbar&apos;"></parent>`)).toBeTrue());
});
32 changes: 31 additions & 1 deletion src/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 "&ampbar", 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");

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}