From b8d079b41372290aa1846e3a780d85d05ab8ffc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Mon, 16 Sep 2019 14:43:22 +0200 Subject: [PATCH] Add trusted types to react on client side (#16157) * Add trusted types to react on client side * Implement changes according to review * Remove support for trusted URLs, change TrustedTypes to trustedTypes * Add support for deprecated trusted URLs * Apply PR suggesstions * Warn only once, remove forgotten check, put it behind a flag * Move comment * Fix PR comments * Fix html toString concatenation * Fix forgotten else branch * Fix PR comments --- .eslintrc.js | 1 + .../src/client/DOMPropertyOperations.js | 14 ++- .../react-dom/src/client/ReactDOMComponent.js | 24 ++++- .../react-dom/src/client/ToStringValue.js | 44 ++++++++ .../__tests__/trustedTypes-test.internal.js | 101 ++++++++++++++++++ packages/react-dom/src/client/setAttribute.js | 35 ++++++ packages/react-dom/src/client/setInnerHTML.js | 43 +++++--- packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.persistent.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + scripts/rollup/validate/eslintrc.cjs.js | 2 + scripts/rollup/validate/eslintrc.fb.js | 2 + scripts/rollup/validate/eslintrc.rn.js | 2 + scripts/rollup/validate/eslintrc.umd.js | 2 + 18 files changed, 259 insertions(+), 19 deletions(-) create mode 100644 packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js create mode 100644 packages/react-dom/src/client/setAttribute.js diff --git a/.eslintrc.js b/.eslintrc.js index c91ba7e6e7fc..603f20a3cacb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -149,5 +149,6 @@ module.exports = { spyOnProd: true, __PROFILE__: true, __UMD__: true, + trustedTypes: true, }, }; diff --git a/packages/react-dom/src/client/DOMPropertyOperations.js b/packages/react-dom/src/client/DOMPropertyOperations.js index 6a2d0f7d244a..72c99e67aa6e 100644 --- a/packages/react-dom/src/client/DOMPropertyOperations.js +++ b/packages/react-dom/src/client/DOMPropertyOperations.js @@ -16,7 +16,9 @@ import { OVERLOADED_BOOLEAN, } from '../shared/DOMProperty'; import sanitizeURL from '../shared/sanitizeURL'; +import {toStringOrTrustedType} from './ToStringValue'; import {disableJavaScriptURLs} from 'shared/ReactFeatureFlags'; +import {setAttribute, setAttributeNS} from './setAttribute'; import type {PropertyInfo} from '../shared/DOMProperty'; @@ -142,7 +144,7 @@ export function setValueForProperty( if (value === null) { node.removeAttribute(attributeName); } else { - node.setAttribute(attributeName, '' + (value: any)); + setAttribute(node, attributeName, toStringOrTrustedType(value)); } } return; @@ -168,19 +170,21 @@ export function setValueForProperty( const {type} = propertyInfo; let attributeValue; if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) { + // If attribute type is boolean, we know for sure it won't be an execution sink + // and we won't require Trusted Type here. attributeValue = ''; } else { // `setAttribute` with objects becomes only `[object]` in IE8/9, // ('' + value) makes it output the correct toString()-value. - attributeValue = '' + (value: any); + attributeValue = toStringOrTrustedType(value); if (propertyInfo.sanitizeURL) { - sanitizeURL(attributeValue); + sanitizeURL(attributeValue.toString()); } } if (attributeNamespace) { - node.setAttributeNS(attributeNamespace, attributeName, attributeValue); + setAttributeNS(node, attributeNamespace, attributeName, attributeValue); } else { - node.setAttribute(attributeName, attributeValue); + setAttribute(node, attributeName, attributeValue); } } } diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index ea178c13ab64..48b52eb195ea 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -85,11 +85,16 @@ import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; +import {toStringOrTrustedType} from './ToStringValue'; -import {enableFlareAPI} from 'shared/ReactFeatureFlags'; +import { + enableFlareAPI, + enableTrustedTypesIntegration, +} from 'shared/ReactFeatureFlags'; let didWarnInvalidHydration = false; let didWarnShadyDOM = false; +let didWarnScriptTags = false; const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML'; const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning'; @@ -422,6 +427,18 @@ export function createElement( // Create the script via .innerHTML so its "parser-inserted" flag is // set to true and it does not execute const div = ownerDocument.createElement('div'); + if (__DEV__) { + if (enableTrustedTypesIntegration && !didWarnScriptTags) { + warning( + false, + 'Encountered a script tag while rendering React component. ' + + 'Scripts inside React components are never executed when rendering ' + + 'on the client. Consider using template tag instead ' + + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).', + ); + didWarnScriptTags = true; + } + } div.innerHTML = ', container); + }).toWarnDev( + 'Warning: Encountered a script tag while rendering React component. ' + + 'Scripts inside React components are never executed when rendering ' + + 'on the client. Consider using template tag instead ' + + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' + + ' in script (at **)', + ); + + // check that the warning is print only once + ReactDOM.render(, container); + }); +}); diff --git a/packages/react-dom/src/client/setAttribute.js b/packages/react-dom/src/client/setAttribute.js new file mode 100644 index 000000000000..8b9b0bd42a65 --- /dev/null +++ b/packages/react-dom/src/client/setAttribute.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {TrustedValue} from './ToStringValue'; + +/** + * Set attribute for a node. The attribute value can be either string or + * Trusted value (if application uses Trusted Types). + */ +export function setAttribute( + node: Element, + attributeName: string, + attributeValue: string | TrustedValue, +) { + node.setAttribute(attributeName, (attributeValue: any)); +} + +/** + * Set attribute with namespace for a node. The attribute value can be either string or + * Trusted value (if application uses Trusted Types). + */ +export function setAttributeNS( + node: Element, + attributeNamespace: string, + attributeName: string, + attributeValue: string | TrustedValue, +) { + node.setAttributeNS(attributeNamespace, attributeName, (attributeValue: any)); +} diff --git a/packages/react-dom/src/client/setInnerHTML.js b/packages/react-dom/src/client/setInnerHTML.js index 0c328b17e0b2..4fa94a4e95c9 100644 --- a/packages/react-dom/src/client/setInnerHTML.js +++ b/packages/react-dom/src/client/setInnerHTML.js @@ -9,6 +9,9 @@ import {Namespaces} from '../shared/DOMNamespaces'; import createMicrosoftUnsafeLocalFunction from '../shared/createMicrosoftUnsafeLocalFunction'; +import warning from 'shared/warning'; +import type {TrustedValue} from './ToStringValue'; +import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags'; // SVG temp container for IE lacking innerHTML let reusableSVGContainer; @@ -22,25 +25,41 @@ let reusableSVGContainer; */ const setInnerHTML = createMicrosoftUnsafeLocalFunction(function( node: Element, - html: string, + html: string | TrustedValue, ): void { // IE does not have innerHTML for SVG nodes, so instead we inject the // new markup in a temp node and then move the child nodes across into // the target node - - if (node.namespaceURI === Namespaces.svg && !('innerHTML' in node)) { - reusableSVGContainer = - reusableSVGContainer || document.createElement('div'); - reusableSVGContainer.innerHTML = '' + html + ''; - const svgNode = reusableSVGContainer.firstChild; - while (node.firstChild) { - node.removeChild(node.firstChild); + if (node.namespaceURI === Namespaces.svg) { + if (enableTrustedTypesIntegration && __DEV__) { + warning( + // $FlowExpectedError - trustedTypes are defined only in some browsers or with polyfill + typeof trustedTypes === 'undefined', + "Using 'dangerouslySetInnerHTML' in an svg element with " + + 'Trusted Types enabled in an Internet Explorer will cause ' + + 'the trusted value to be converted to string. Assigning string ' + + "to 'innerHTML' will throw an error if Trusted Types are enforced. " + + "You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " + + 'on the enclosing div instead.', + ); } - while (svgNode.firstChild) { - node.appendChild(svgNode.firstChild); + if (!('innerHTML' in node)) { + reusableSVGContainer = + reusableSVGContainer || document.createElement('div'); + reusableSVGContainer.innerHTML = + '' + html.valueOf().toString() + ''; + const svgNode = reusableSVGContainer.firstChild; + while (node.firstChild) { + node.removeChild(node.firstChild); + } + while (svgNode.firstChild) { + node.appendChild(svgNode.firstChild); + } + } else { + node.innerHTML = (html: any); } } else { - node.innerHTML = html; + node.innerHTML = (html: any); } }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 4121f9b6d7ef..c7789e65fa0e 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -100,3 +100,5 @@ export const warnAboutStringRefs = false; export const disableLegacyContext = false; export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; + +export const enableTrustedTypesIntegration = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index c29f0c210a4d..12e700b0dc46 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -43,6 +43,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; export const disableLegacyContext = false; export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; +export const enableTrustedTypesIntegration = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 876063c2c928..cd7a5d91b13d 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -38,6 +38,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; export const disableLegacyContext = false; export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; +export const enableTrustedTypesIntegration = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index dc6540b5d819..0fb0154932e2 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -38,6 +38,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; export const disableLegacyContext = false; export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; +export const enableTrustedTypesIntegration = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 7aa962fa46e9..c30a8a2081be 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -38,6 +38,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; export const disableLegacyContext = false; export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; +export const enableTrustedTypesIntegration = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 608385059998..61144269cbca 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -36,6 +36,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; export const disableLegacyContext = false; export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; +export const enableTrustedTypesIntegration = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 7abb9c69e730..a3d2295974de 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -22,6 +22,7 @@ export const { enableUserBlockingEvents, disableLegacyContext, disableSchedulerTimeoutBasedOnReactExpirationTime, + enableTrustedTypesIntegration, warnAboutStringRefs, warnAboutDefaultPropsOnFunctionComponents, } = require('ReactFeatureFlags'); diff --git a/scripts/rollup/validate/eslintrc.cjs.js b/scripts/rollup/validate/eslintrc.cjs.js index 2347fbb6cfe8..6d1089b73788 100644 --- a/scripts/rollup/validate/eslintrc.cjs.js +++ b/scripts/rollup/validate/eslintrc.cjs.js @@ -21,6 +21,8 @@ module.exports = { process: true, setImmediate: true, Buffer: true, + // Trusted Types + trustedTypes: true, // Scheduler profiling SharedArrayBuffer: true, diff --git a/scripts/rollup/validate/eslintrc.fb.js b/scripts/rollup/validate/eslintrc.fb.js index a32b7e38f0a3..57daf502c221 100644 --- a/scripts/rollup/validate/eslintrc.fb.js +++ b/scripts/rollup/validate/eslintrc.fb.js @@ -22,6 +22,8 @@ module.exports = { // Node.js Server Rendering setImmediate: true, Buffer: true, + // Trusted Types + trustedTypes: true, // Scheduler profiling SharedArrayBuffer: true, diff --git a/scripts/rollup/validate/eslintrc.rn.js b/scripts/rollup/validate/eslintrc.rn.js index f211d9573f42..9906dfb5b8ab 100644 --- a/scripts/rollup/validate/eslintrc.rn.js +++ b/scripts/rollup/validate/eslintrc.rn.js @@ -21,6 +21,8 @@ module.exports = { // Fabric. See https://github.com/facebook/react/pull/15490 // for more information nativeFabricUIManager: true, + // Trusted Types + trustedTypes: true, // Scheduler profiling SharedArrayBuffer: true, diff --git a/scripts/rollup/validate/eslintrc.umd.js b/scripts/rollup/validate/eslintrc.umd.js index 5b96526bba8e..57645c7a34c6 100644 --- a/scripts/rollup/validate/eslintrc.umd.js +++ b/scripts/rollup/validate/eslintrc.umd.js @@ -24,6 +24,8 @@ module.exports = { define: true, require: true, global: true, + // Trusted Types + trustedTypes: true, // Scheduler profiling SharedArrayBuffer: true,