@samitouri / QOS-React-2 / commits / b8d079b413

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

Emanuel Tesař committed Sep 16, 2019 at 14:43 UTC b8d079b41372290aa1846e3a780d85d05ab8ffc1
18 files changed +259 -19
.eslintrc.js
+1
@@ -149,5 +149,6 @@ module.exports = {
149 spyOnProd: true,
150 __PROFILE__: true,
151 __UMD__: true,
152 + trustedTypes: true,
153 },
154 };
packages/react-dom/src/client/DOMPropertyOperations.js
+9 -5
@@ -16,7 +16,9 @@ import {
16 OVERLOADED_BOOLEAN,
17 } from '../shared/DOMProperty';
18 import sanitizeURL from '../shared/sanitizeURL';
19 +import {toStringOrTrustedType} from './ToStringValue';
20 import {disableJavaScriptURLs} from 'shared/ReactFeatureFlags';
21 +import {setAttribute, setAttributeNS} from './setAttribute';
22
23 import type {PropertyInfo} from '../shared/DOMProperty';
24
@@ -142,7 +144,7 @@ export function setValueForProperty(
144 if (value === null) {
145 node.removeAttribute(attributeName);
146 } else {
145 - node.setAttribute(attributeName, '' + (value: any));
147 + setAttribute(node, attributeName, toStringOrTrustedType(value));
148 }
149 }
150 return;
@@ -168,19 +170,21 @@ export function setValueForProperty(
170 const {type} = propertyInfo;
171 let attributeValue;
172 if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) {
173 + // If attribute type is boolean, we know for sure it won't be an execution sink
174 + // and we won't require Trusted Type here.
175 attributeValue = '';
176 } else {
177 // `setAttribute` with objects becomes only `[object]` in IE8/9,
178 // ('' + value) makes it output the correct toString()-value.
175 - attributeValue = '' + (value: any);
179 + attributeValue = toStringOrTrustedType(value);
180 if (propertyInfo.sanitizeURL) {
177 - sanitizeURL(attributeValue);
181 + sanitizeURL(attributeValue.toString());
182 }
183 }
184 if (attributeNamespace) {
181 - node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
185 + setAttributeNS(node, attributeNamespace, attributeName, attributeValue);
186 } else {
183 - node.setAttribute(attributeName, attributeValue);
187 + setAttribute(node, attributeName, attributeValue);
188 }
189 }
190 }
packages/react-dom/src/client/ReactDOMComponent.js
+22 -2
@@ -85,11 +85,16 @@ import possibleStandardNames from '../shared/possibleStandardNames';
85 import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
86 import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
87 import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
88 +import {toStringOrTrustedType} from './ToStringValue';
89
89 -import {enableFlareAPI} from 'shared/ReactFeatureFlags';
90 +import {
91 + enableFlareAPI,
92 + enableTrustedTypesIntegration,
93 +} from 'shared/ReactFeatureFlags';
94
95 let didWarnInvalidHydration = false;
96 let didWarnShadyDOM = false;
97 +let didWarnScriptTags = false;
98
99 const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';
100 const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';
@@ -422,6 +427,18 @@ export function createElement(
427 // Create the script via .innerHTML so its "parser-inserted" flag is
428 // set to true and it does not execute
429 const div = ownerDocument.createElement('div');
430 + if (__DEV__) {
431 + if (enableTrustedTypesIntegration && !didWarnScriptTags) {
432 + warning(
433 + false,
434 + 'Encountered a script tag while rendering React component. ' +
435 + 'Scripts inside React components are never executed when rendering ' +
436 + 'on the client. Consider using template tag instead ' +
437 + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
438 + );
439 + didWarnScriptTags = true;
440 + }
441 + }
442 div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
443 // This is guaranteed to yield a script element.
444 const firstChild = ((div.firstChild: any): HTMLScriptElement);
@@ -776,7 +793,10 @@ export function diffProperties(
793 const lastHtml = lastProp ? lastProp[HTML] : undefined;
794 if (nextHtml != null) {
795 if (lastHtml !== nextHtml) {
779 - (updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
796 + (updatePayload = updatePayload || []).push(
797 + propKey,
798 + toStringOrTrustedType(nextHtml),
799 + );
800 }
801 } else {
802 // TODO: It might be too late to clear this if we have children
packages/react-dom/src/client/ToStringValue.js
+44
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
11 +
12 export opaque type ToStringValue =
13 | boolean
14 | number
@@ -35,3 +37,45 @@ export function getToStringValue(value: mixed): ToStringValue {
37 return '';
38 }
39 }
40 +
41 +/**
42 + * Returns true only if Trusted Types are available in global object and the value is a trusted type.
43 + */
44 +let isTrustedTypesValue: (value: any) => boolean;
45 +// $FlowExpectedError - TrustedTypes are defined only in some browsers or with polyfill
46 +if (enableTrustedTypesIntegration && typeof trustedTypes !== 'undefined') {
47 + isTrustedTypesValue = (value: any) =>
48 + trustedTypes.isHTML(value) ||
49 + trustedTypes.isScript(value) ||
50 + trustedTypes.isScriptURL(value) ||
51 + // TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204
52 + (trustedTypes.isURL && trustedTypes.isURL(value));
53 +} else {
54 + isTrustedTypesValue = () => false;
55 +}
56 +
57 +/** Trusted value is a wrapper for "safe" values which can be assigned to DOM execution sinks. */
58 +export opaque type TrustedValue: {toString(): string, valueOf(): string} = {
59 + toString(): string,
60 + valueOf(): string,
61 +};
62 +
63 +/**
64 + * We allow passing objects with toString method as element attributes or in dangerouslySetInnerHTML
65 + * and we do validations that the value is safe. Once we do validation we want to use the validated
66 + * value instead of the object (because object.toString may return something else on next call).
67 + *
68 + * If application uses Trusted Types we don't stringify trusted values, but preserve them as objects.
69 + */
70 +export function toStringOrTrustedType(value: any): string | TrustedValue {
71 + if (
72 + enableTrustedTypesIntegration &&
73 + // fast-path string values as it's most frequent usage of the function
74 + typeof value !== 'string' &&
75 + isTrustedTypesValue(value)
76 + ) {
77 + return value;
78 + } else {
79 + return '' + value;
80 + }
81 +}
packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js new
+101
@@ -0,0 +1,101 @@
1 +describe('when Trusted Types are available in global object', () => {
2 + let React;
3 + let ReactDOM;
4 + let ReactFeatureFlags;
5 + let container;
6 +
7 + beforeEach(() => {
8 + container = document.createElement('div');
9 + window.trustedTypes = {
10 + isHTML: () => true,
11 + isScript: () => false,
12 + isScriptURL: () => false,
13 + };
14 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
15 + ReactFeatureFlags.enableTrustedTypesIntegration = true;
16 + React = require('react');
17 + ReactDOM = require('react-dom');
18 + });
19 +
20 + afterEach(() => {
21 + delete window.trustedTypes;
22 + ReactFeatureFlags.enableTrustedTypesIntegration = false;
23 + });
24 +
25 + it('should not stringify trusted values', () => {
26 + const trustedObject = {toString: () => 'I look like a trusted object'};
27 + class Component extends React.Component {
28 + state = {inner: undefined};
29 + render() {
30 + return <div dangerouslySetInnerHTML={{__html: this.state.inner}} />;
31 + }
32 + }
33 +
34 + const isHTMLSpy = jest.spyOn(window.trustedTypes, ['isHTML']);
35 + const instance = ReactDOM.render(<Component />, container);
36 + instance.setState({inner: trustedObject});
37 +
38 + expect(container.firstChild.innerHTML).toBe(trustedObject.toString());
39 + expect(isHTMLSpy).toHaveBeenCalledWith(trustedObject);
40 + });
41 +
42 + describe('dangerouslySetInnerHTML in svg elements in Internet Explorer', () => {
43 + let innerHTMLDescriptor;
44 +
45 + // simulate svg elements in Internet Explorer which don't have 'innerHTML' property
46 + beforeEach(() => {
47 + innerHTMLDescriptor = Object.getOwnPropertyDescriptor(
48 + Element.prototype,
49 + 'innerHTML',
50 + );
51 + delete Element.prototype.innerHTML;
52 + Object.defineProperty(
53 + HTMLDivElement.prototype,
54 + 'innerHTML',
55 + innerHTMLDescriptor,
56 + );
57 + });
58 +
59 + afterEach(() => {
60 + delete HTMLDivElement.prototype.innerHTML;
61 + Object.defineProperty(
62 + Element.prototype,
63 + 'innerHTML',
64 + innerHTMLDescriptor,
65 + );
66 + });
67 +
68 + it('should log a warning', () => {
69 + class Component extends React.Component {
70 + render() {
71 + return <svg dangerouslySetInnerHTML={{__html: 'unsafe html'}} />;
72 + }
73 + }
74 + expect(() => {
75 + ReactDOM.render(<Component />, container);
76 + }).toWarnDev(
77 + "Warning: Using 'dangerouslySetInnerHTML' in an svg element with " +
78 + 'Trusted Types enabled in an Internet Explorer will cause ' +
79 + 'the trusted value to be converted to string. Assigning string ' +
80 + "to 'innerHTML' will throw an error if Trusted Types are enforced. " +
81 + "You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " +
82 + 'on the enclosing div instead.',
83 + );
84 + });
85 + });
86 +
87 + it('should warn once when rendering script tag in jsx on client', () => {
88 + expect(() => {
89 + ReactDOM.render(<script>alert("I am not executed")</script>, container);
90 + }).toWarnDev(
91 + 'Warning: Encountered a script tag while rendering React component. ' +
92 + 'Scripts inside React components are never executed when rendering ' +
93 + 'on the client. Consider using template tag instead ' +
94 + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' +
95 + ' in script (at **)',
96 + );
97 +
98 + // check that the warning is print only once
99 + ReactDOM.render(<script>alert("I am not executed")</script>, container);
100 + });
101 +});
packages/react-dom/src/client/setAttribute.js new
+35
@@ -0,0 +1,35 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {TrustedValue} from './ToStringValue';
11 +
12 +/**
13 + * Set attribute for a node. The attribute value can be either string or
14 + * Trusted value (if application uses Trusted Types).
15 + */
16 +export function setAttribute(
17 + node: Element,
18 + attributeName: string,
19 + attributeValue: string | TrustedValue,
20 +) {
21 + node.setAttribute(attributeName, (attributeValue: any));
22 +}
23 +
24 +/**
25 + * Set attribute with namespace for a node. The attribute value can be either string or
26 + * Trusted value (if application uses Trusted Types).
27 + */
28 +export function setAttributeNS(
29 + node: Element,
30 + attributeNamespace: string,
31 + attributeName: string,
32 + attributeValue: string | TrustedValue,
33 +) {
34 + node.setAttributeNS(attributeNamespace, attributeName, (attributeValue: any));
35 +}
packages/react-dom/src/client/setInnerHTML.js
+31 -12
@@ -9,6 +9,9 @@
9
10 import {Namespaces} from '../shared/DOMNamespaces';
11 import createMicrosoftUnsafeLocalFunction from '../shared/createMicrosoftUnsafeLocalFunction';
12 +import warning from 'shared/warning';
13 +import type {TrustedValue} from './ToStringValue';
14 +import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
15
16 // SVG temp container for IE lacking innerHTML
17 let reusableSVGContainer;
@@ -22,25 +25,41 @@ let reusableSVGContainer;
25 */
26 const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
27 node: Element,
25 - html: string,
28 + html: string | TrustedValue,
29 ): void {
30 // IE does not have innerHTML for SVG nodes, so instead we inject the
31 // new markup in a temp node and then move the child nodes across into
32 // the target node
30 -
31 - if (node.namespaceURI === Namespaces.svg && !('innerHTML' in node)) {
32 - reusableSVGContainer =
33 - reusableSVGContainer || document.createElement('div');
34 - reusableSVGContainer.innerHTML = '<svg>' + html + '</svg>';
35 - const svgNode = reusableSVGContainer.firstChild;
36 - while (node.firstChild) {
37 - node.removeChild(node.firstChild);
33 + if (node.namespaceURI === Namespaces.svg) {
34 + if (enableTrustedTypesIntegration && __DEV__) {
35 + warning(
36 + // $FlowExpectedError - trustedTypes are defined only in some browsers or with polyfill
37 + typeof trustedTypes === 'undefined',
38 + "Using 'dangerouslySetInnerHTML' in an svg element with " +
39 + 'Trusted Types enabled in an Internet Explorer will cause ' +
40 + 'the trusted value to be converted to string. Assigning string ' +
41 + "to 'innerHTML' will throw an error if Trusted Types are enforced. " +
42 + "You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " +
43 + 'on the enclosing div instead.',
44 + );
45 }
39 - while (svgNode.firstChild) {
40 - node.appendChild(svgNode.firstChild);
46 + if (!('innerHTML' in node)) {
47 + reusableSVGContainer =
48 + reusableSVGContainer || document.createElement('div');
49 + reusableSVGContainer.innerHTML =
50 + '<svg>' + html.valueOf().toString() + '</svg>';
51 + const svgNode = reusableSVGContainer.firstChild;
52 + while (node.firstChild) {
53 + node.removeChild(node.firstChild);
54 + }
55 + while (svgNode.firstChild) {
56 + node.appendChild(svgNode.firstChild);
57 + }
58 + } else {
59 + node.innerHTML = (html: any);
60 }
61 } else {
43 - node.innerHTML = html;
62 + node.innerHTML = (html: any);
63 }
64 });
65
packages/shared/ReactFeatureFlags.js
+2
@@ -100,3 +100,5 @@ export const warnAboutStringRefs = false;
100 export const disableLegacyContext = false;
101
102 export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
103 +
104 +export const enableTrustedTypesIntegration = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -43,6 +43,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
43 export const warnAboutStringRefs = false;
44 export const disableLegacyContext = false;
45 export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
46 +export const enableTrustedTypesIntegration = false;
47
48 // Only used in www builds.
49 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -38,6 +38,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
38 export const warnAboutStringRefs = false;
39 export const disableLegacyContext = false;
40 export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
41 +export const enableTrustedTypesIntegration = false;
42
43 // Only used in www builds.
44 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -38,6 +38,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
38 export const warnAboutStringRefs = false;
39 export const disableLegacyContext = false;
40 export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
41 +export const enableTrustedTypesIntegration = false;
42
43 // Only used in www builds.
44 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -38,6 +38,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
38 export const warnAboutStringRefs = false;
39 export const disableLegacyContext = false;
40 export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
41 +export const enableTrustedTypesIntegration = false;
42
43 // Only used in www builds.
44 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -36,6 +36,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
36 export const warnAboutStringRefs = false;
37 export const disableLegacyContext = false;
38 export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
39 +export const enableTrustedTypesIntegration = false;
40
41 // Only used in www builds.
42 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -22,6 +22,7 @@ export const {
22 enableUserBlockingEvents,
23 disableLegacyContext,
24 disableSchedulerTimeoutBasedOnReactExpirationTime,
25 + enableTrustedTypesIntegration,
26 warnAboutStringRefs,
27 warnAboutDefaultPropsOnFunctionComponents,
28 } = require('ReactFeatureFlags');
scripts/rollup/validate/eslintrc.cjs.js
+2
@@ -21,6 +21,8 @@ module.exports = {
21 process: true,
22 setImmediate: true,
23 Buffer: true,
24 + // Trusted Types
25 + trustedTypes: true,
26
27 // Scheduler profiling
28 SharedArrayBuffer: true,
scripts/rollup/validate/eslintrc.fb.js
+2
@@ -22,6 +22,8 @@ module.exports = {
22 // Node.js Server Rendering
23 setImmediate: true,
24 Buffer: true,
25 + // Trusted Types
26 + trustedTypes: true,
27
28 // Scheduler profiling
29 SharedArrayBuffer: true,
scripts/rollup/validate/eslintrc.rn.js
+2
@@ -21,6 +21,8 @@ module.exports = {
21 // Fabric. See https://github.com/facebook/react/pull/15490
22 // for more information
23 nativeFabricUIManager: true,
24 + // Trusted Types
25 + trustedTypes: true,
26
27 // Scheduler profiling
28 SharedArrayBuffer: true,
scripts/rollup/validate/eslintrc.umd.js
+2
@@ -24,6 +24,8 @@ module.exports = {
24 define: true,
25 require: true,
26 global: true,
27 + // Trusted Types
28 + trustedTypes: true,
29
30 // Scheduler profiling
31 SharedArrayBuffer: true,