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,