From 10641a642f87dd8a1c3564d1f839a328b6c6f2c7 Mon Sep 17 00:00:00 2001 From: Tom Usborne Date: Fri, 20 Dec 2024 11:50:59 -0500 Subject: [PATCH] Improve html attribute escaping function --- src/hoc/withHtmlAttributes.js | 13 +----- src/tests/unit/sanitizeHtmlAttribute.test.js | 49 ++++++++++++++++++++ src/utils/sanitizeHtmlAttribute.js | 25 ++++++++++ 3 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 src/tests/unit/sanitizeHtmlAttribute.test.js create mode 100644 src/utils/sanitizeHtmlAttribute.js diff --git a/src/hoc/withHtmlAttributes.js b/src/hoc/withHtmlAttributes.js index 222525713..27e6cd812 100644 --- a/src/hoc/withHtmlAttributes.js +++ b/src/hoc/withHtmlAttributes.js @@ -7,6 +7,7 @@ import { applyFilters } from '@wordpress/hooks'; import { useUpdateEffect } from 'react-use'; import { convertInlineStyleStringToObject } from '@utils/convertInlineStyleStringToObject'; +import { sanitizeHtmlAttribute } from '@utils/sanitizeHtmlAttribute'; export const booleanAttributes = [ 'allowfullscreen', @@ -60,16 +61,6 @@ function shallowEqual( obj1, obj2 ) { return true; } -const sanitizeAttributeValue = ( value ) => { - // Replace characters like &, <, >, " with their HTML entity equivalents - return value.toString() - .replace( /&/g, '&' ) - .replace( //g, '>' ) - .replace( /"/g, '"' ) - .replace( /'/g, ''' ); -}; - export function withHtmlAttributes( WrappedComponent ) { return ( ( props ) => { const { @@ -88,7 +79,7 @@ export function withHtmlAttributes( WrappedComponent ) { const isSavingPost = useSelect( ( select ) => select( 'core/editor' ).isSavingPost() ); const { style = '', href, ...otherAttributes } = htmlAttributes; const escapedAttributes = Object.keys( otherAttributes ).reduce( ( acc, key ) => { - acc[ key ] = sanitizeAttributeValue( otherAttributes[ key ] ); + acc[ key ] = sanitizeHtmlAttribute( otherAttributes[ key ] ); return acc; }, {} ); const [ processedStyle, setProcessedStyle ] = useState( style ); diff --git a/src/tests/unit/sanitizeHtmlAttribute.test.js b/src/tests/unit/sanitizeHtmlAttribute.test.js new file mode 100644 index 000000000..45d51793b --- /dev/null +++ b/src/tests/unit/sanitizeHtmlAttribute.test.js @@ -0,0 +1,49 @@ +import { sanitizeHtmlAttribute } from '../../utils/sanitizeHtmlAttribute'; + +describe( 'sanitize HTML attribute', () => { + it( 'should convert a number to a string', () => { + const value = sanitizeHtmlAttribute( 500 ); + expect( value ).toEqual( '500' ); + } ); + + it( 'should convert an object to a string', () => { + const value = sanitizeHtmlAttribute( { foo: 'bar' } ); + expect( value ).toEqual( '{"foo":"bar"}' ); + } ); + + it( 'should convert an array to a string', () => { + const value = sanitizeHtmlAttribute( [ 'foo', 'bar' ] ); + expect( value ).toEqual( '["foo","bar"]' ); + } ); + + it( 'should handle undefined', () => { + const value = sanitizeHtmlAttribute( undefined ); + expect( value ).toEqual( '' ); + } ); + + it( 'should handle null', () => { + const value = sanitizeHtmlAttribute( null ); + expect( value ).toEqual( '' ); + } ); + + it( 'should escape HTML special characters', () => { + const value = sanitizeHtmlAttribute( 'foo & "quote" \'single\'' ); + expect( value ).toEqual( 'foo & <bar> "quote" 'single'' ); + } ); + + it( 'should handle boolean values', () => { + expect( sanitizeHtmlAttribute( true ) ).toEqual( 'true' ); + expect( sanitizeHtmlAttribute( false ) ).toEqual( 'false' ); + } ); + + it( 'should handle special number values', () => { + expect( sanitizeHtmlAttribute( NaN ) ).toEqual( 'NaN' ); + expect( sanitizeHtmlAttribute( Infinity ) ).toEqual( 'Infinity' ); + expect( sanitizeHtmlAttribute( -Infinity ) ).toEqual( '-Infinity' ); + } ); + + it( 'should handle empty string', () => { + const value = sanitizeHtmlAttribute( '' ); + expect( value ).toEqual( '' ); + } ); +} ); diff --git a/src/utils/sanitizeHtmlAttribute.js b/src/utils/sanitizeHtmlAttribute.js new file mode 100644 index 000000000..328004663 --- /dev/null +++ b/src/utils/sanitizeHtmlAttribute.js @@ -0,0 +1,25 @@ +export const sanitizeHtmlAttribute = ( value ) => { + if ( null === value || undefined === value ) { + return ''; + } + + let stringValue = ''; + + if ( 'object' === typeof value ) { + try { + stringValue = JSON.stringify( value ); + } catch ( e ) { + return ''; + } + } else { + stringValue = String( value ); + } + + // Replace characters like &, <, >, " with their HTML entity equivalents + return stringValue + .replace( /&/g, '&' ) + .replace( //g, '>' ) + .replace( /"/g, '"' ) + .replace( /'/g, ''' ); +};