diff --git a/packages/element/src/serialize.js b/packages/element/src/serialize.js index 0903118948e5a7..d05eb753e7f313 100644 --- a/packages/element/src/serialize.js +++ b/packages/element/src/serialize.js @@ -227,6 +227,19 @@ const CSS_PROPERTIES_SUPPORTS_UNITLESS = new Set( [ 'zoom', ] ); +/** + * Regular expression matching invalid attribute names. + * + * "Attribute names must consist of one or more characters other than controls, + * U+0020 SPACE, U+0022 ("), U+0027 ('), U+003E (>), U+002F (/), U+003D (=), + * and noncharacters." + * + * @link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + * + * @type {RegExp} + */ +const REGEXP_INVALID_ATTRIBUTE_NAME = /[\u007F-\u009F "'>/="\uFDD0-\uFDEF]/; + /** * Returns a string with ampersands escaped. Note that this is an imperfect * implementation, where only ampersands which do not appear as a pattern of @@ -301,6 +314,17 @@ export const escapeHTML = flowRight( [ escapeLessThan, ] ); +/** + * Returns true if the given attribute name is valid, or false otherwise. + * + * @param {string} name Attribute name to test. + * + * @return {boolean} Whether attribute is valid. + */ +export function isValidAttributeName( name ) { + return ! REGEXP_INVALID_ATTRIBUTE_NAME.test( name ); +} + /** * Returns true if the specified string is prefixed by one of an array of * possible prefixes. @@ -556,6 +580,10 @@ export function renderAttributes( props ) { for ( const key in props ) { const attribute = getNormalAttributeName( key ); + if ( ! isValidAttributeName( attribute ) ) { + continue; + } + let value = getNormalAttributeValue( key, props[ key ] ); // If value is not of serializeable type, skip. diff --git a/packages/element/src/test/serialize.js b/packages/element/src/test/serialize.js index b7b88408414541..44e83734eae8a8 100644 --- a/packages/element/src/test/serialize.js +++ b/packages/element/src/test/serialize.js @@ -8,6 +8,7 @@ import { noop } from 'lodash'; */ import { Component, + createElement, Fragment, RawHTML, } from '../'; @@ -18,6 +19,7 @@ import serialize, { escapeAttribute, escapeHTML, hasPrefix, + isValidAttributeName, renderElement, renderNativeComponent, renderComponent, @@ -71,7 +73,49 @@ describe( 'escapeHTML', () => { testEscapeLessThan( escapeHTML ); } ); +describe( 'isValidAttributeName', () => { + it( 'should return false for attribute with controls', () => { + const result = isValidAttributeName( 'bad\u007F' ); + + expect( result ).toBe( false ); + } ); + + it( 'should return false for attribute with non-permitted characters', () => { + const result = isValidAttributeName( 'bad"' ); + + expect( result ).toBe( false ); + } ); + + it( 'should return false for attribute with noncharacters', () => { + const result = isValidAttributeName( 'bad\uFDD0' ); + + expect( result ).toBe( false ); + } ); + + it( 'should return true for valid attribute name', () => { + const result = isValidAttributeName( 'good' ); + + expect( result ).toBe( true ); + } ); +} ); + describe( 'serialize()', () => { + it( 'should allow only valid attribute names', () => { + const element = createElement( + 'div', + { + 'notok\u007F': 'bad', + 'notok"': 'bad', + ok: 'good', + 'notok\uFDD0': 'bad', + }, + ); + + const result = serialize( element ); + + expect( result ).toBe( '
' ); + } ); + it( 'should render with context', () => { class Provider extends Component { getChildContext() {