Follow-up to initial Trusted Types support (#16795)
* Follow-up to initial Trusted Types support * Fast-path both strings and numbers * Move shared objects out of every test
Dan Abramov committed
Sep 17, 2019 at 16:06 UTC
8e0c574122b3d3eadcffaec1223e95d479b01c6e
4 files changed
+198
-60
packages/react-dom/src/client/ToStringValue.js
+17
-27
@@ -38,22 +38,6 @@ export function getToStringValue(value: mixed): ToStringValue {
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
-
41
/** Trusted value is a wrapper for "safe" values which can be assigned to DOM execution sinks. */
42
export opaque type TrustedValue: {toString(): string, valueOf(): string} = {
43
toString(): string,
@@ -67,15 +51,21 @@ export opaque type TrustedValue: {toString(): string, valueOf(): string} = {
51
*
52
* If application uses Trusted Types we don't stringify trusted values, but preserve them as objects.
53
*/
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
- }
54
+export let toStringOrTrustedType: any => string | TrustedValue = toString;
55
+if (enableTrustedTypesIntegration && typeof trustedTypes !== 'undefined') {
56
+ const isHTML = trustedTypes.isHTML;
57
+ const isScript = trustedTypes.isScript;
58
+ const isScriptURL = trustedTypes.isScriptURL;
59
+ // TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204
60
+ const isURL = trustedTypes.isURL ? trustedTypes.isURL : value => false;
61
+ toStringOrTrustedType = value => {
62
+ if (
63
+ typeof value === 'object' &&
64
+ (isHTML(value) || isScript(value) || isScriptURL(value) || isURL(value))
65
+ ) {
66
+ // Pass Trusted Types through.
67
+ return value;
68
+ }
69
+ return toString(value);
70
+ };
71
}
packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js
+154
-15
@@ -1,13 +1,28 @@
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
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
describe('when Trusted Types are available in global object', () => {
13
let React;
14
let ReactDOM;
15
let ReactFeatureFlags;
16
let container;
17
+ let ttObject1;
18
+ let ttObject2;
19
20
beforeEach(() => {
21
+ jest.resetModules();
22
container = document.createElement('div');
23
+ const fakeTTObjects = new Set();
24
window.trustedTypes = {
10
- isHTML: () => true,
25
+ isHTML: value => fakeTTObjects.has(value),
26
isScript: () => false,
27
isScriptURL: () => false,
28
};
@@ -15,28 +30,151 @@ describe('when Trusted Types are available in global object', () => {
30
ReactFeatureFlags.enableTrustedTypesIntegration = true;
31
React = require('react');
32
ReactDOM = require('react-dom');
33
+ ttObject1 = {
34
+ toString() {
35
+ return '<b>Hi</b>';
36
+ },
37
+ };
38
+ ttObject2 = {
39
+ toString() {
40
+ return '<b>Bye</b>';
41
+ },
42
+ };
43
+ fakeTTObjects.add(ttObject1);
44
+ fakeTTObjects.add(ttObject2);
45
});
46
47
afterEach(() => {
48
delete window.trustedTypes;
22
- ReactFeatureFlags.enableTrustedTypesIntegration = false;
49
});
50
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
- }
51
+ it('should not stringify trusted values for dangerouslySetInnerHTML', () => {
52
+ let innerHTMLDescriptor = Object.getOwnPropertyDescriptor(
53
+ Element.prototype,
54
+ 'innerHTML',
55
+ );
56
+ try {
57
+ const innerHTMLCalls = [];
58
+ Object.defineProperty(Element.prototype, 'innerHTML', {
59
+ get() {
60
+ return innerHTMLDescriptor.get.apply(this, arguments);
61
+ },
62
+ set(value) {
63
+ innerHTMLCalls.push(value);
64
+ return innerHTMLDescriptor.set.apply(this, arguments);
65
+ },
66
+ });
67
+ ReactDOM.render(
68
+ <div dangerouslySetInnerHTML={{__html: ttObject1}} />,
69
+ container,
70
+ );
71
+ expect(container.innerHTML).toBe('<div><b>Hi</b></div>');
72
+ expect(innerHTMLCalls.length).toBe(1);
73
+ // Ensure it didn't get stringified when passed to a DOM sink:
74
+ expect(innerHTMLCalls[0]).toBe(ttObject1);
75
+
76
+ innerHTMLCalls.length = 0;
77
+ ReactDOM.render(
78
+ <div dangerouslySetInnerHTML={{__html: ttObject2}} />,
79
+ container,
80
+ );
81
+ expect(container.innerHTML).toBe('<div><b>Bye</b></div>');
82
+ expect(innerHTMLCalls.length).toBe(1);
83
+ // Ensure it didn't get stringified when passed to a DOM sink:
84
+ expect(innerHTMLCalls[0]).toBe(ttObject2);
85
+ } finally {
86
+ Object.defineProperty(
87
+ Element.prototype,
88
+ 'innerHTML',
89
+ innerHTMLDescriptor,
90
+ );
91
+ }
92
+ });
93
+
94
+ it('should not stringify trusted values for setAttribute (unknown attribute)', () => {
95
+ let setAttribute = Element.prototype.setAttribute;
96
+ try {
97
+ const setAttributeCalls = [];
98
+ Element.prototype.setAttribute = function(name, value) {
99
+ setAttributeCalls.push([this, name.toLowerCase(), value]);
100
+ return setAttribute.apply(this, arguments);
101
+ };
102
+ ReactDOM.render(<div data-foo={ttObject1} />, container);
103
+ expect(container.innerHTML).toBe('<div data-foo="<b>Hi</b>"></div>');
104
+ expect(setAttributeCalls.length).toBe(1);
105
+ expect(setAttributeCalls[0][0]).toBe(container.firstChild);
106
+ expect(setAttributeCalls[0][1]).toBe('data-foo');
107
+ // Ensure it didn't get stringified when passed to a DOM sink:
108
+ expect(setAttributeCalls[0][2]).toBe(ttObject1);
109
+
110
+ setAttributeCalls.length = 0;
111
+ ReactDOM.render(<div data-foo={ttObject2} />, container);
112
+ expect(setAttributeCalls.length).toBe(1);
113
+ expect(setAttributeCalls[0][0]).toBe(container.firstChild);
114
+ expect(setAttributeCalls[0][1]).toBe('data-foo');
115
+ // Ensure it didn't get stringified when passed to a DOM sink:
116
+ expect(setAttributeCalls[0][2]).toBe(ttObject2);
117
+ } finally {
118
+ Element.prototype.setAttribute = setAttribute;
119
}
120
+ });
121
+
122
+ it('should not stringify trusted values for setAttribute (known attribute)', () => {
123
+ let setAttribute = Element.prototype.setAttribute;
124
+ try {
125
+ const setAttributeCalls = [];
126
+ Element.prototype.setAttribute = function(name, value) {
127
+ setAttributeCalls.push([this, name.toLowerCase(), value]);
128
+ return setAttribute.apply(this, arguments);
129
+ };
130
+ ReactDOM.render(<div className={ttObject1} />, container);
131
+ expect(container.innerHTML).toBe('<div class="<b>Hi</b>"></div>');
132
+ expect(setAttributeCalls.length).toBe(1);
133
+ expect(setAttributeCalls[0][0]).toBe(container.firstChild);
134
+ expect(setAttributeCalls[0][1]).toBe('class');
135
+ // Ensure it didn't get stringified when passed to a DOM sink:
136
+ expect(setAttributeCalls[0][2]).toBe(ttObject1);
137
+
138
+ setAttributeCalls.length = 0;
139
+ ReactDOM.render(<div className={ttObject2} />, container);
140
+ expect(setAttributeCalls.length).toBe(1);
141
+ expect(setAttributeCalls[0][0]).toBe(container.firstChild);
142
+ expect(setAttributeCalls[0][1]).toBe('class');
143
+ // Ensure it didn't get stringified when passed to a DOM sink:
144
+ expect(setAttributeCalls[0][2]).toBe(ttObject2);
145
+ } finally {
146
+ Element.prototype.setAttribute = setAttribute;
147
+ }
148
+ });
149
34
- const isHTMLSpy = jest.spyOn(window.trustedTypes, ['isHTML']);
35
- const instance = ReactDOM.render(<Component />, container);
36
- instance.setState({inner: trustedObject});
150
+ it('should not stringify trusted values for setAttributeNS', () => {
151
+ let setAttributeNS = Element.prototype.setAttributeNS;
152
+ try {
153
+ const setAttributeNSCalls = [];
154
+ Element.prototype.setAttributeNS = function(ns, name, value) {
155
+ setAttributeNSCalls.push([this, ns, name, value]);
156
+ return setAttributeNS.apply(this, arguments);
157
+ };
158
+ ReactDOM.render(<svg xlinkHref={ttObject1} />, container);
159
+ expect(container.innerHTML).toBe('<svg xlink:href="<b>Hi</b>"></svg>');
160
+ expect(setAttributeNSCalls.length).toBe(1);
161
+ expect(setAttributeNSCalls[0][0]).toBe(container.firstChild);
162
+ expect(setAttributeNSCalls[0][1]).toBe('http://www.w3.org/1999/xlink');
163
+ expect(setAttributeNSCalls[0][2]).toBe('xlink:href');
164
+ // Ensure it didn't get stringified when passed to a DOM sink:
165
+ expect(setAttributeNSCalls[0][3]).toBe(ttObject1);
166
38
- expect(container.firstChild.innerHTML).toBe(trustedObject.toString());
39
- expect(isHTMLSpy).toHaveBeenCalledWith(trustedObject);
167
+ setAttributeNSCalls.length = 0;
168
+ ReactDOM.render(<svg xlinkHref={ttObject2} />, container);
169
+ expect(setAttributeNSCalls.length).toBe(1);
170
+ expect(setAttributeNSCalls[0][0]).toBe(container.firstChild);
171
+ expect(setAttributeNSCalls[0][1]).toBe('http://www.w3.org/1999/xlink');
172
+ expect(setAttributeNSCalls[0][2]).toBe('xlink:href');
173
+ // Ensure it didn't get stringified when passed to a DOM sink:
174
+ expect(setAttributeNSCalls[0][3]).toBe(ttObject2);
175
+ } finally {
176
+ Element.prototype.setAttributeNS = setAttributeNS;
177
+ }
178
});
179
180
describe('dangerouslySetInnerHTML in svg elements in Internet Explorer', () => {
@@ -81,6 +219,7 @@ describe('when Trusted Types are available in global object', () => {
219
"You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " +
220
'on the enclosing div instead.',
221
);
222
+ expect(container.innerHTML).toBe('<svg>unsafe html</svg>');
223
});
224
});
225
@@ -95,7 +234,7 @@ describe('when Trusted Types are available in global object', () => {
234
' in script (at **)',
235
);
236
98
- // check that the warning is print only once
237
+ // check that the warning is printed only once
238
ReactDOM.render(<script>alert("I am not executed")</script>, container);
239
});
240
});
packages/react-dom/src/client/setInnerHTML.js
+19
-18
@@ -27,23 +27,26 @@ const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
27
node: Element,
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
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
- );
31
+ if (__DEV__) {
32
+ if (enableTrustedTypesIntegration) {
33
+ // TODO: reconsider the text of this warning and when it should show
34
+ // before enabling the feature flag.
35
+ warning(
36
+ typeof trustedTypes === 'undefined',
37
+ "Using 'dangerouslySetInnerHTML' in an svg element with " +
38
+ 'Trusted Types enabled in an Internet Explorer will cause ' +
39
+ 'the trusted value to be converted to string. Assigning string ' +
40
+ "to 'innerHTML' will throw an error if Trusted Types are enforced. " +
41
+ "You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " +
42
+ 'on the enclosing div instead.',
43
+ );
44
+ }
45
}
46
if (!('innerHTML' in node)) {
47
+ // IE does not have innerHTML for SVG nodes, so instead we inject the
48
+ // new markup in a temp node and then move the child nodes across into
49
+ // the target node
50
reusableSVGContainer =
51
reusableSVGContainer || document.createElement('div');
52
reusableSVGContainer.innerHTML =
@@ -55,12 +58,10 @@ const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
58
while (svgNode.firstChild) {
59
node.appendChild(svgNode.firstChild);
60
}
58
- } else {
59
- node.innerHTML = (html: any);
61
+ return;
62
}
61
- } else {
62
- node.innerHTML = (html: any);
63
}
64
+ node.innerHTML = (html: any);
65
});
66
67
export default setInnerHTML;
scripts/flow/environment.js
+8
@@ -16,6 +16,14 @@ declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{
16
inject: ?((stuff: Object) => void)
17
};*/
18
19
+declare var trustedTypes: {|
20
+ isHTML: (value: any) => boolean,
21
+ isScript: (value: any) => boolean,
22
+ isScriptURL: (value: any) => boolean,
23
+ // TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204
24
+ isURL?: (value: any) => boolean,
25
+|};
26
+
27
// ReactFeatureFlags www fork
28
declare module 'ReactFeatureFlags' {
29
declare module.exports: any;