From 6f7e183553206afa2ca21914bf388e019b4acfdc Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Tue, 9 Nov 2021 08:18:35 -0700 Subject: [PATCH] fix(get-selector): do not URL encode or token escape attribute selectors (#3215) * fix(get-selector): do not URL encode or token escape attribute selectors * escape * more escape * tests --- lib/core/utils/get-selector.js | 28 +++++++++++++----- test/core/utils/get-selector.js | 52 ++++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/lib/core/utils/get-selector.js b/lib/core/utils/get-selector.js index aa8e787ba2..02c192f8ec 100644 --- a/lib/core/utils/get-selector.js +++ b/lib/core/utils/get-selector.js @@ -26,6 +26,23 @@ const ignoredAttributes = [ 'aria-valuenow' ]; const MAXATTRIBUTELENGTH = 31; +const attrCharsRegex = /([\\"])/g; +const newlineChars = /(\r\n|\r|\n)/g; + +/** + * Escape an attribute selector string. + * @param {String} str + * @return {String} + */ +function escapeAttribute(str) { + return ( + str + // @see https://www.py4u.net/discuss/286669 + .replace(attrCharsRegex, '\\$1') + // @see https://stackoverflow.com/a/20354013/2124254 + .replace(newlineChars, '\\a ') + ); +} /** * get the attribute name and value as a string @@ -40,21 +57,16 @@ function getAttributeNameValue(node, at) { if (name.indexOf('href') !== -1 || name.indexOf('src') !== -1) { const friendly = getFriendlyUriEnd(node.getAttribute(name)); if (friendly) { - const value = encodeURI(friendly); - if (value) { - atnv = escapeSelector(at.name) + '$="' + escapeSelector(value) + '"'; - } else { - return; - } + atnv = escapeSelector(at.name) + '$="' + escapeAttribute(friendly) + '"'; } else { atnv = escapeSelector(at.name) + '="' + - escapeSelector(node.getAttribute(name)) + + escapeAttribute(node.getAttribute(name)) + '"'; } } else { - atnv = escapeSelector(name) + '="' + escapeSelector(at.value) + '"'; + atnv = escapeSelector(name) + '="' + escapeAttribute(at.value) + '"'; } return atnv; } diff --git a/test/core/utils/get-selector.js b/test/core/utils/get-selector.js index a694e33570..c6cd4381cd 100644 --- a/test/core/utils/get-selector.js +++ b/test/core/utils/get-selector.js @@ -494,8 +494,8 @@ describe('axe.utils.getSelector', function() { img2.setAttribute('src', '//deque.com/logo.png'); fixtureSetup([link1, link2, img1, img2]); - assert.equal(axe.utils.getSelector(link2), 'a[href$="about\\/"]'); - assert.equal(axe.utils.getSelector(img2), 'img[src$="logo\\.png"]'); + assert.equal(axe.utils.getSelector(link2), 'a[href$="about/"]'); + assert.equal(axe.utils.getSelector(img2), 'img[src$="logo.png"]'); }); it('should escape href attributes', function() { @@ -508,10 +508,49 @@ describe('axe.utils.getSelector', function() { fixtureSetup([link1, link2]); assert.equal( axe.utils.getSelector(link2), - 'a[href="\\/\\/deque\\.com\\/child\\/\\ \\a \\a \\a "]' + 'a[href="//deque.com/child/ \\a \\a \\a "]' ); }); + it('should not URL encode or token escape href attribute', function() { + var link1 = document.createElement('a'); + link1.setAttribute('href', '3 Seater'); + + var link2 = document.createElement('a'); + link2.setAttribute('href', '1 Seater'); + + var expected = 'a[href$="1 Seater"]'; + fixtureSetup([link1, link2]); + assert.equal(axe.utils.getSelector(link2), expected); + assert.isTrue(axe.utils.matchesSelector(link2, expected)); + }); + + it('should escape certain special characters in attribute', function() { + var div1 = document.createElement('div'); + div1.setAttribute('data-thing', 'foobar'); + + var div2 = document.createElement('div'); + div2.setAttribute('data-thing', '!@#$%^&*()_+[]\\;\',./{}|:"<>?'); + + var expected = 'div[data-thing="!@#$%^&*()_+[]\\\\;\',./{}|:\\"<>?"]'; + fixtureSetup([div1, div2]); + assert.equal(axe.utils.getSelector(div2), expected); + assert.isTrue(axe.utils.matchesSelector(div2, expected)); + }); + + it('should escape newline characters in attribute', function() { + var div1 = document.createElement('div'); + div1.setAttribute('data-thing', 'foobar'); + + var div2 = document.createElement('div'); + div2.setAttribute('data-thing', ' \n\n\n'); + + var expected = 'div[data-thing=" \\a \\a \\a "]'; + fixtureSetup([div1, div2]); + assert.equal(axe.utils.getSelector(div2), expected); + assert.isTrue(axe.utils.matchesSelector(div2, expected)); + }); + it('should not generate universal selectors', function() { var node = document.createElement('div'); node.setAttribute('role', 'menuitem'); @@ -530,11 +569,8 @@ describe('axe.utils.getSelector', function() { node2.setAttribute('href', href2); fixtureSetup([node1, node2]); - assert.include(axe.utils.getSelector(node1), 'mars2\\.html\\?a\\=be_bold'); - assert.include( - axe.utils.getSelector(node2), - 'mars2\\.html\\?a\\=be_italic' - ); + assert.include(axe.utils.getSelector(node1), 'mars2.html?a=be_bold'); + assert.include(axe.utils.getSelector(node2), 'mars2.html?a=be_italic'); }); // shadow DOM v1 - note: v0 is compatible with this code, so no need