Skip to content

Commit c146aee

Browse files
committed
[Trusted Types] Don't stringify DOM attributes (facebook#19588 redo)
Summary: This is a resubmit of facebook#19588 since it was never merged and closed in error. However, the issue it fixes is still relevant and will unblock rollout of the TT feature flag internally. Copying the original PR message here: ----- Instead of using Trusted Types feature flag, assume that the browser would stringify the attributes, so that React-DOM doesn't have to (making interpolation into node attributes "just work" regardless of the Trusted Types enforcement and availability) . Currently only IE<=9 does not stringify attributes. This PR implicitly drops the support for IE 9. For attributes undergoing sanitizeURL, the value is stringified in sanitizeURL function, unless enableTrustedTypesIntegration is true and the value is an immutable TrustedScriptURL value. This ascertains that objects with custom toString() function cannot be used to bypass the sanitization (now that DOMPropertyOperations don't stringify on their own). Fixes facebook#19587. ## Summary The PR decouples the attribute stringification from the Trusted Types logic, dropping the former completely. It was only used as a workaround for a IE <=9 browser bug. A small improvement for Trusted Types would be that it moves the most important functionality from behind the flag - i.e. allows most React apps to pass trusted types into DOM sinks without enabling the feature flag. Some rare functionality and dev warnings are still gated on the flag, but those are unrelated to the stringification issue. ## Test Plan Appropriate tests are added.
1 parent c5b9375 commit c146aee

File tree

3 files changed

+83
-21
lines changed

3 files changed

+83
-21
lines changed

packages/react-dom-bindings/src/client/DOMPropertyOperations.js

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
*/
99

1010
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
11-
import {
12-
enableTrustedTypesIntegration,
13-
enableCustomElementPropertySupport,
14-
} from 'shared/ReactFeatureFlags';
11+
import {enableCustomElementPropertySupport} from 'shared/ReactFeatureFlags';
1512
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
1613
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
1714

@@ -133,10 +130,7 @@ export function setValueForAttribute(
133130
if (__DEV__) {
134131
checkAttributeStringCoercion(value, name);
135132
}
136-
node.setAttribute(
137-
name,
138-
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
139-
);
133+
node.setAttribute(name, (value: any));
140134
}
141135
}
142136

@@ -161,10 +155,7 @@ export function setValueForKnownAttribute(
161155
if (__DEV__) {
162156
checkAttributeStringCoercion(value, name);
163157
}
164-
node.setAttribute(
165-
name,
166-
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
167-
);
158+
node.setAttribute(name, (value: any));
168159
}
169160

170161
export function setValueForNamespacedAttribute(
@@ -189,11 +180,7 @@ export function setValueForNamespacedAttribute(
189180
if (__DEV__) {
190181
checkAttributeStringCoercion(value, name);
191182
}
192-
node.setAttributeNS(
193-
namespace,
194-
name,
195-
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
196-
);
183+
node.setAttributeNS(namespace, name, (value: any));
197184
}
198185

199186
export function setValueForPropertyOnCustomComponent(

packages/react-dom-bindings/src/shared/sanitizeURL.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
* @flow
88
*/
99

10-
import {disableJavaScriptURLs} from 'shared/ReactFeatureFlags';
10+
import {
11+
disableJavaScriptURLs,
12+
enableTrustedTypesIntegration,
13+
} from 'shared/ReactFeatureFlags';
1114

1215
// A javascript: URL can contain leading C0 control or \u0020 SPACE,
1316
// and any newline or tab are filtered out as if they're not part of the URL.
@@ -28,6 +31,14 @@ function sanitizeURL<T>(url: T): T | string {
2831
// We should never have symbols here because they get filtered out elsewhere.
2932
// eslint-disable-next-line react-internal/safe-string-coercion
3033
const stringifiedURL = '' + (url: any);
34+
if (
35+
!enableTrustedTypesIntegration ||
36+
typeof trustedTypes === 'undefined' ||
37+
!trustedTypes.isScriptURL(url)
38+
) {
39+
// Coerce to a string, unless we know it's an immutable TrustedScriptURL object.
40+
url = stringifiedURL;
41+
}
3142
if (disableJavaScriptURLs) {
3243
if (isJavaScriptProtocol.test(stringifiedURL)) {
3344
// Return a different javascript: url that doesn't cause any side-effects and just

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

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,37 @@ describe('when Trusted Types are available in global object', () => {
1616
let container;
1717
let ttObject1;
1818
let ttObject2;
19+
let fakeTTObjects;
20+
21+
const expectToReject = fn => {
22+
let msg;
23+
try {
24+
fn();
25+
} catch (x) {
26+
msg = x.message;
27+
}
28+
expect(msg).toContain(
29+
'React has blocked a javascript: URL as a security precaution.',
30+
);
31+
};
1932

2033
beforeEach(() => {
2134
jest.resetModules();
2235
container = document.createElement('div');
23-
const fakeTTObjects = new Set();
36+
fakeTTObjects = new Set();
2437
window.trustedTypes = {
2538
isHTML: function (value) {
2639
if (this !== window.trustedTypes) {
2740
throw new Error(this);
2841
}
2942
return fakeTTObjects.has(value);
3043
},
31-
isScript: () => false,
32-
isScriptURL: () => false,
44+
isScript: value => fakeTTObjects.has(value),
45+
isScriptURL: value => fakeTTObjects.has(value),
3346
};
3447
ReactFeatureFlags = require('shared/ReactFeatureFlags');
3548
ReactFeatureFlags.enableTrustedTypesIntegration = true;
49+
ReactFeatureFlags.disableJavaScriptURLs = true;
3650
React = require('react');
3751
ReactDOM = require('react-dom');
3852
ttObject1 = {
@@ -152,6 +166,56 @@ describe('when Trusted Types are available in global object', () => {
152166
}
153167
});
154168

169+
it('should not stringify attributes that go through sanitizeURL', () => {
170+
const setAttribute = Element.prototype.setAttribute;
171+
try {
172+
const setAttributeCalls = [];
173+
Element.prototype.setAttribute = function (name, value) {
174+
setAttributeCalls.push([this, name.toLowerCase(), value]);
175+
return setAttribute.apply(this, arguments);
176+
};
177+
const trustedScriptUrlHttps = {
178+
toString: () => 'https://ok.example',
179+
};
180+
fakeTTObjects.add(trustedScriptUrlHttps);
181+
// It's not a matching type (under Trusted Types a.href a string will do),
182+
// but a.href undergoes URL and we're only testing if the value was
183+
// passed unmodified to setAttribute.
184+
ReactDOM.render(<a href={trustedScriptUrlHttps} />, container);
185+
expect(setAttributeCalls.length).toBe(1);
186+
expect(setAttributeCalls[0][0]).toBe(container.firstChild);
187+
expect(setAttributeCalls[0][1]).toBe('href');
188+
// Ensure it didn't get stringified when passed to a DOM sink:
189+
expect(setAttributeCalls[0][2]).toBe(trustedScriptUrlHttps);
190+
} finally {
191+
Element.prototype.setAttribute = setAttribute;
192+
}
193+
});
194+
195+
it('should sanitize attributes even if they are Trusted Types', () => {
196+
const setAttribute = Element.prototype.setAttribute;
197+
try {
198+
const setAttributeCalls = [];
199+
Element.prototype.setAttribute = function (name, value) {
200+
setAttributeCalls.push([this, name.toLowerCase(), value]);
201+
return setAttribute.apply(this, arguments);
202+
};
203+
const trustedScriptUrlJavascript = {
204+
// eslint-disable-next-line no-script-url
205+
toString: () => 'javascript:notfine',
206+
};
207+
fakeTTObjects.add(trustedScriptUrlJavascript);
208+
// Assert that the URL sanitization will correctly unwrap and verify the
209+
// value.
210+
expectToReject(() => {
211+
ReactDOM.render(<a href={trustedScriptUrlJavascript} />, container);
212+
});
213+
expect(setAttributeCalls.length).toBe(0);
214+
} finally {
215+
Element.prototype.setAttribute = setAttribute;
216+
}
217+
});
218+
155219
it('should not stringify trusted values for setAttributeNS', () => {
156220
const setAttributeNS = Element.prototype.setAttributeNS;
157221
try {

0 commit comments

Comments
 (0)