Skip to content

Commit 8081590

Browse files
committed
[Trusted Types] Don't stringify DOM attributes (facebook#19588 redo)
Summary: This is a resubmit of facebook#19588. Fixes facebook#19587.
1 parent c5b9375 commit 8081590

File tree

3 files changed

+82
-21
lines changed

3 files changed

+82
-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: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,35 @@ describe('when Trusted Types are available in global object', () => {
1717
let ttObject1;
1818
let ttObject2;
1919

20+
const expectToReject = fn => {
21+
let msg;
22+
try {
23+
fn();
24+
} catch (x) {
25+
msg = x.message;
26+
}
27+
expect(msg).toContain(
28+
'React has blocked a javascript: URL as a security precaution.',
29+
);
30+
};
31+
2032
beforeEach(() => {
2133
jest.resetModules();
2234
container = document.createElement('div');
23-
const fakeTTObjects = new Set();
35+
fakeTTObjects = new Set();
2436
window.trustedTypes = {
2537
isHTML: function (value) {
2638
if (this !== window.trustedTypes) {
2739
throw new Error(this);
2840
}
2941
return fakeTTObjects.has(value);
3042
},
31-
isScript: () => false,
32-
isScriptURL: () => false,
43+
isScript: value => fakeTTObjects.has(value),
44+
isScriptURL: value => fakeTTObjects.has(value),
3345
};
3446
ReactFeatureFlags = require('shared/ReactFeatureFlags');
3547
ReactFeatureFlags.enableTrustedTypesIntegration = true;
48+
ReactFeatureFlags.disableJavaScriptURLs = true;
3649
React = require('react');
3750
ReactDOM = require('react-dom');
3851
ttObject1 = {
@@ -152,6 +165,56 @@ describe('when Trusted Types are available in global object', () => {
152165
}
153166
});
154167

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

0 commit comments

Comments
 (0)