Skip to content

Commit 8e0c574

Browse files
authored
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
1 parent 3af05de commit 8e0c574

File tree

4 files changed

+198
-60
lines changed

4 files changed

+198
-60
lines changed

packages/react-dom/src/client/ToStringValue.js

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,6 @@ export function getToStringValue(value: mixed): ToStringValue {
3838
}
3939
}
4040

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-
5741
/** Trusted value is a wrapper for "safe" values which can be assigned to DOM execution sinks. */
5842
export opaque type TrustedValue: {toString(): string, valueOf(): string} = {
5943
toString(): string,
@@ -67,15 +51,21 @@ export opaque type TrustedValue: {toString(): string, valueOf(): string} = {
6751
*
6852
* If application uses Trusted Types we don't stringify trusted values, but preserve them as objects.
6953
*/
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+
};
8171
}

packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js

Lines changed: 154 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,180 @@
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+
112
describe('when Trusted Types are available in global object', () => {
213
let React;
314
let ReactDOM;
415
let ReactFeatureFlags;
516
let container;
17+
let ttObject1;
18+
let ttObject2;
619

720
beforeEach(() => {
21+
jest.resetModules();
822
container = document.createElement('div');
23+
const fakeTTObjects = new Set();
924
window.trustedTypes = {
10-
isHTML: () => true,
25+
isHTML: value => fakeTTObjects.has(value),
1126
isScript: () => false,
1227
isScriptURL: () => false,
1328
};
1429
ReactFeatureFlags = require('shared/ReactFeatureFlags');
1530
ReactFeatureFlags.enableTrustedTypesIntegration = true;
1631
React = require('react');
1732
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);
1845
});
1946

2047
afterEach(() => {
2148
delete window.trustedTypes;
22-
ReactFeatureFlags.enableTrustedTypesIntegration = false;
2349
});
2450

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;
32119
}
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+
});
33149

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);
37166

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+
}
40178
});
41179

42180
describe('dangerouslySetInnerHTML in svg elements in Internet Explorer', () => {
@@ -81,6 +219,7 @@ describe('when Trusted Types are available in global object', () => {
81219
"You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " +
82220
'on the enclosing div instead.',
83221
);
222+
expect(container.innerHTML).toBe('<svg>unsafe html</svg>');
84223
});
85224
});
86225

@@ -95,7 +234,7 @@ describe('when Trusted Types are available in global object', () => {
95234
' in script (at **)',
96235
);
97236

98-
// check that the warning is print only once
237+
// check that the warning is printed only once
99238
ReactDOM.render(<script>alert("I am not executed")</script>, container);
100239
});
101240
});

packages/react-dom/src/client/setInnerHTML.js

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,26 @@ const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
2727
node: Element,
2828
html: string | TrustedValue,
2929
): 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
3330
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+
}
4545
}
4646
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
4750
reusableSVGContainer =
4851
reusableSVGContainer || document.createElement('div');
4952
reusableSVGContainer.innerHTML =
@@ -55,12 +58,10 @@ const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
5558
while (svgNode.firstChild) {
5659
node.appendChild(svgNode.firstChild);
5760
}
58-
} else {
59-
node.innerHTML = (html: any);
61+
return;
6062
}
61-
} else {
62-
node.innerHTML = (html: any);
6363
}
64+
node.innerHTML = (html: any);
6465
});
6566

6667
export default setInnerHTML;

scripts/flow/environment.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{
1616
inject: ?((stuff: Object) => void)
1717
};*/
1818

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+
1927
// ReactFeatureFlags www fork
2028
declare module 'ReactFeatureFlags' {
2129
declare module.exports: any;

0 commit comments

Comments
 (0)