diff --git a/packages/react-dom-bindings/src/client/ReactDOMInput.js b/packages/react-dom-bindings/src/client/ReactDOMInput.js index 9bacf8e046314..ee5d5a207c21b 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMInput.js +++ b/packages/react-dom-bindings/src/client/ReactDOMInput.js @@ -18,6 +18,7 @@ import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags'; import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import type {ToStringValue} from './ToStringValue'; +import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes'; let didWarnValueDefaultValue = false; let didWarnCheckedDefaultChecked = false; @@ -309,7 +310,9 @@ export function restoreControlledInputState(element: Element, props: Object) { checkAttributeStringCoercion(name, 'name'); } const group = queryRoot.querySelectorAll( - 'input[name=' + JSON.stringify('' + name) + '][type="radio"]', + 'input[name="' + + escapeSelectorAttributeValueInsideDoubleQuotes('' + name) + + '"][type="radio"]', ); for (let i = 0; i < group.length; i++) { diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 2315c00547953..d806f14d85af6 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -107,6 +107,7 @@ import { getValueDescriptorExpectingObjectForWarning, getValueDescriptorExpectingEnumForWarning, } from '../shared/ReactDOMResourceValidation'; +import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes'; export type Type = string; export type Props = { @@ -2513,11 +2514,13 @@ function styleTagPropsFromRawProps( function getStyleKey(href: string) { const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(href); - return `href~="${limitedEscapedHref}"`; + return `href="${limitedEscapedHref}"`; } -function getStyleTagSelectorFromKey(key: string) { - return `style[data-${key}]`; +function getStyleTagSelector(href: string) { + const limitedEscapedHref = + escapeSelectorAttributeValueInsideDoubleQuotes(href); + return `style[data-href~="${limitedEscapedHref}"]`; } function getStylesheetSelectorFromKey(key: string) { @@ -2602,11 +2605,10 @@ export function acquireResource( switch (resource.type) { case 'style': { const qualifiedProps: StyleTagQualifyingProps = props; - const key = getStyleKey(qualifiedProps.href); // Attempt to hydrate instance from DOM let instance: null | Instance = hoistableRoot.querySelector( - getStyleTagSelectorFromKey(key), + getStyleTagSelector(qualifiedProps.href), ); if (instance) { resource.instance = instance; @@ -2987,19 +2989,6 @@ export function unmountHoistable(instance: Instance): void { (instance.parentNode: any).removeChild(instance); } -// When passing user input into querySelector(All) the embedded string must not alter -// the semantics of the query. This escape function is safe to use when we know the -// provided value is going to be wrapped in double quotes as part of an attribute selector -// Do not use it anywhere else -// we escape double quotes and backslashes -const escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n\"\\]/g; -function escapeSelectorAttributeValueInsideDoubleQuotes(value: string): string { - return value.replace( - escapeSelectorAttributeValueInsideDoubleQuotesRegex, - ch => '\\' + ch.charCodeAt(0).toString(16), - ); -} - export function isHostHoistableType( type: string, props: RawProps, diff --git a/packages/react-dom-bindings/src/client/escapeSelectorAttributeValueInsideDoubleQuotes.js b/packages/react-dom-bindings/src/client/escapeSelectorAttributeValueInsideDoubleQuotes.js new file mode 100644 index 0000000000000..47c0db23c7b48 --- /dev/null +++ b/packages/react-dom-bindings/src/client/escapeSelectorAttributeValueInsideDoubleQuotes.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// When passing user input into querySelector(All) the embedded string must not alter +// the semantics of the query. This escape function is safe to use when we know the +// provided value is going to be wrapped in double quotes as part of an attribute selector +// Do not use it anywhere else +// we escape double quotes and backslashes +const escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n\"\\]/g; +export default function escapeSelectorAttributeValueInsideDoubleQuotes( + value: string, +): string { + return value.replace( + escapeSelectorAttributeValueInsideDoubleQuotesRegex, + ch => '\\' + ch.charCodeAt(0).toString(16) + ' ', + ); +}