@samitouri / QOS-React-2 / commits / 8e0c574122

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;