diff --git a/.eslintrc b/.eslintrc index 93cde8712f..a45c5c4801 100644 --- a/.eslintrc +++ b/.eslintrc @@ -70,6 +70,12 @@ { "selector": "MemberExpression[property.name=tagName]", "message": "Don't use node.tagName, use node.nodeName instead." + }, + { + // node.attributes can be clobbered so is unsafe to use + // @see https://github.com/dequelabs/axe-core/pull/1432 + "selector": "MemberExpression[object.name=node][property.name=attributes]", + "message": "Don't use node.attributes, use node.hasAttributes() or axe.utils.getNodeAttributes(node) instead." } ] } diff --git a/lib/checks/aria/allowed-attr.js b/lib/checks/aria/allowed-attr.js index 0e1b2d2fa8..b9c8fd7928 100644 --- a/lib/checks/aria/allowed-attr.js +++ b/lib/checks/aria/allowed-attr.js @@ -6,7 +6,7 @@ var attr, attrName, allowed, role = node.getAttribute('role'), - attrs = node.attributes; + attrs = axe.utils.getNodeAttributes(node); if (!role) { role = axe.commons.aria.implicitRole(node); diff --git a/lib/checks/aria/unsupportedattr.js b/lib/checks/aria/unsupportedattr.js index b485d5019e..6c8aca65a3 100644 --- a/lib/checks/aria/unsupportedattr.js +++ b/lib/checks/aria/unsupportedattr.js @@ -2,7 +2,7 @@ const nodeName = node.nodeName.toUpperCase(); const lookupTable = axe.commons.aria.lookupTable; const role = axe.commons.aria.getRole(node); -const unsupportedAttrs = Array.from(node.attributes) +const unsupportedAttrs = Array.from(axe.utils.getNodeAttributes(node)) .filter(({ name }) => { const attribute = lookupTable.attributes[name]; diff --git a/lib/checks/aria/valid-attr-value.js b/lib/checks/aria/valid-attr-value.js index e84a02d06d..548d52e805 100644 --- a/lib/checks/aria/valid-attr-value.js +++ b/lib/checks/aria/valid-attr-value.js @@ -5,7 +5,7 @@ var invalid = [], var attr, attrName, - attrs = node.attributes; + attrs = axe.utils.getNodeAttributes(node); var skipAttrs = ['aria-errormessage']; diff --git a/lib/checks/aria/valid-attr.js b/lib/checks/aria/valid-attr.js index d9a46f287f..638ad83114 100644 --- a/lib/checks/aria/valid-attr.js +++ b/lib/checks/aria/valid-attr.js @@ -4,7 +4,7 @@ var invalid = [], aria = /^aria-/; var attr, - attrs = node.attributes; + attrs = axe.utils.getNodeAttributes(node); for (var i = 0, l = attrs.length; i < l; i++) { attr = attrs[i].name; diff --git a/lib/commons/aria/roles.js b/lib/commons/aria/roles.js index ee55770335..cecd4aeb1b 100644 --- a/lib/commons/aria/roles.js +++ b/lib/commons/aria/roles.js @@ -188,7 +188,7 @@ aria.implicitRole = function(node) { return null; } - var nodeAttributes = node.attributes; + var nodeAttributes = axe.utils.getNodeAttributes(node); var ariaAttributes = []; /* Get all aria-attributes defined for this node */ diff --git a/lib/core/utils/get-node-attributes.js b/lib/core/utils/get-node-attributes.js new file mode 100644 index 0000000000..675926d955 --- /dev/null +++ b/lib/core/utils/get-node-attributes.js @@ -0,0 +1,21 @@ +/* global axe */ + +/** + * Return the list of attributes of a node. + * @method getNodeAttributes + * @memberof axe.utils + * @param {Element} node + * @returns {NamedNodeMap} + */ +axe.utils.getNodeAttributes = function getNodeAttributes(node) { + // eslint-disable-next-line no-restricted-syntax + if (node.attributes instanceof window.NamedNodeMap) { + // eslint-disable-next-line no-restricted-syntax + return node.attributes; + } + + // if the attributes property is not of type NamedNodeMap then the DOM + // has been clobbered. E.g.
. + // We can clone the node to isolate it and then return the attributes + return node.cloneNode(false).attributes; +}; diff --git a/lib/core/utils/get-selector.js b/lib/core/utils/get-selector.js index e0af47ad0d..7f2e4775dd 100644 --- a/lib/core/utils/get-selector.js +++ b/lib/core/utils/get-selector.js @@ -119,8 +119,8 @@ axe.utils.getSelectorData = function(domTree) { } // count all the filtered attributes - if (node.attributes) { - Array.from(node.attributes) + if (node.hasAttributes()) { + Array.from(axe.utils.getNodeAttributes(node)) .filter(filterAttributes) .forEach(at => { let atnv = getAttributeNameValue(node, at); @@ -238,8 +238,8 @@ function uncommonAttributes(node, selectorData) { let attData = selectorData.attributes; let tagData = selectorData.tags; - if (node.attributes) { - Array.from(node.attributes) + if (node.hasAttributes()) { + Array.from(axe.utils.getNodeAttributes(node)) .filter(filterAttributes) .forEach(at => { const atnv = getAttributeNameValue(node, at); diff --git a/lib/rules/aria-allowed-attr-matches.js b/lib/rules/aria-allowed-attr-matches.js index 782c99b465..09b5d69edc 100644 --- a/lib/rules/aria-allowed-attr-matches.js +++ b/lib/rules/aria-allowed-attr-matches.js @@ -1,6 +1,6 @@ const aria = /^aria-/; if (node.hasAttributes()) { - let attrs = node.attributes; + let attrs = axe.utils.getNodeAttributes(node); for (let i = 0, l = attrs.length; i < l; i++) { if (aria.test(attrs[i].name)) { return true; diff --git a/lib/rules/aria-has-attr-matches.js b/lib/rules/aria-has-attr-matches.js index a9bdbc4a5a..c12eb40965 100644 --- a/lib/rules/aria-has-attr-matches.js +++ b/lib/rules/aria-has-attr-matches.js @@ -1,6 +1,6 @@ var aria = /^aria-/; if (node.hasAttributes()) { - var attrs = node.attributes; + var attrs = axe.utils.getNodeAttributes(node); for (var i = 0, l = attrs.length; i < l; i++) { if (aria.test(attrs[i].name)) { return true; diff --git a/test/core/utils/get-node-attributes.js b/test/core/utils/get-node-attributes.js new file mode 100644 index 0000000000..bd35887d94 --- /dev/null +++ b/test/core/utils/get-node-attributes.js @@ -0,0 +1,26 @@ +describe('axe.utils.getNodeAttributes', function() { + 'use strict'; + + it('should return the list of attributes', function() { + var node = document.createElement('div'); + node.setAttribute('class', 'foo bar'); + var actual = axe.utils.getNodeAttributes(node); + assert.isTrue(actual instanceof window.NamedNodeMap); + assert.equal(actual.length, 1); + assert.equal(actual[0].name, 'class'); + }); + + it('should return the list of attributes when the DOM is clobbered', function() { + var node = document.createElement('form'); + node.setAttribute('id', '123'); + node.innerHTML = ''; + + // eslint-disable-next-line no-restricted-syntax + assert.isFalse(node.attributes instanceof window.NamedNodeMap); + + var actual = axe.utils.getNodeAttributes(node); + assert.isTrue(actual instanceof window.NamedNodeMap); + assert.equal(actual.length, 1); + assert.equal(actual[0].name, 'id'); + }); +});