Skip to content

Commit 624c0d2

Browse files
committed
[RFC] Trusted sources for React elements.
This is a proposal for tightening React's XSS security model as referenced by #3473. React Elements are simply JS objects of a particular shape. Because React applications are frequently assembling large trees of these elements and including user data, it's possible for a malicious user to insert data that appears like a React Element and therefore render arbitrary and potentially dangerous markup. Previous versions of React used instanceof, but that limited the ability to use multiple Reacts and tightly coupled JSX to React which we wanted to avoid. This proposal replaces `{_isReactElement: true}` with `{_source: "randomstring"}`. Using React.createElement() automatically adds this _source, but JSX could generate regular object bodies with `{_source: React.getSourceID()}`. In order to use multiple Reacts, a new API `React.trustSource(sourceID)` is added. You can imagine using a different React instance in a webworker and using `React.trustSource(sourceIDFromCallingWorkersReactGetSourceID)`. To preserve back-compat, the default behavior proposed is to dangerously trust all sources, but to warn! If this proposal lands, then I imagine a future version of React will make the default behavior to not trust unknown sources, removing warnings.
1 parent d402bd3 commit 624c0d2

File tree

3 files changed

+278
-1
lines changed

3 files changed

+278
-1
lines changed

src/browser/ui/React.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ var React = {
7676
unmountComponentAtNode: ReactMount.unmountComponentAtNode,
7777
isValidElement: ReactElement.isValidElement,
7878
withContext: ReactContext.withContext,
79+
getSourceID: ReactElement.getSourceID,
80+
trustSource: ReactElement.trustSource,
81+
dangerouslyTrustAllSources: ReactElement.dangerouslyTrustAllSources,
7982

8083
// Hook for JSX spread, don't use this for anything else.
8184
__spread: assign

src/classic/element/ReactElement.js

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,35 @@ function defineWarningProperty(object, key) {
6060
*/
6161
var useMutationMembrane = false;
6262

63+
/**
64+
* React instances own source ID is a randomly generated string. It does not
65+
* have to be globally unique, just difficult to guess.
66+
*/
67+
var ownSourceID =
68+
(typeof crypto === 'object' && crypto.getRandomValues ?
69+
crypto.getRandomValues(new Uint32Array(1))[0] :
70+
~(Math.random() * (1 << 31))
71+
).toString(36);
72+
73+
/**
74+
* If trustSource() is called, becomes an mapping of sourceIDs we trust as
75+
* valid React Elements.
76+
*/
77+
var trustedSourceIDs; // ?{ [sourceID: string]: true }
78+
79+
/**
80+
* If true, dangerously trust all sources. If false, only trust explicit
81+
* sources.
82+
*
83+
* If null, trust all sources but warn if not explicitly trusted.
84+
*/
85+
var trustAllSources = null; // ?Boolean
86+
87+
/**
88+
* Updated to true if a warning is logged so we don't spam console.
89+
*/
90+
var hasWarnedAboutUntrustedSource;
91+
6392
/**
6493
* Warn for mutations.
6594
*
@@ -139,13 +168,55 @@ var ReactElement = function(type, key, ref, owner, context, props) {
139168
// We intentionally don't expose the function on the constructor property.
140169
// ReactElement should be indistinguishable from a plain object.
141170
ReactElement.prototype = {
171+
_source: ownSourceID,
142172
_isReactElement: true
143173
};
144174

145175
if (__DEV__) {
146176
defineMutationMembrane(ReactElement.prototype);
147177
}
148178

179+
180+
/**
181+
* Return this React module's source ID.
182+
*/
183+
ReactElement.getSourceID = function() {
184+
return ownSourceID;
185+
};
186+
187+
/**
188+
* Allows this React module to trust React elements produced from another React
189+
* module, potentially from a Server or from another Realm (iframe, webworker).
190+
*/
191+
ReactElement.trustSource = function(sourceID) {
192+
if (!trustedSourceIDs) {
193+
trustedSourceIDs = {};
194+
}
195+
trustedSourceIDs[sourceID] = true;
196+
// Calling trustSource implies using React's trusted source.
197+
// TODO: remove in a future version when security is on by default and
198+
// trustAllSources is no longer a ?Boolean.
199+
if (trustAllSources === null) {
200+
trustAllSources = false;
201+
}
202+
};
203+
204+
/**
205+
* Trust React elements regardless of their source, or even if they have an
206+
* unknown source. Using this method may be helpful during a refactor to support
207+
* React's security model, but should be avoided as it could allow XSS attacks
208+
* in certain conditions.
209+
*
210+
* For backwards compatibility, the default behavior is to trust all sources,
211+
* but to warn in __DEV__ when rendering a React component from an unknown
212+
* source. In a future version of React only explicitly trusted sources may
213+
* provide React components.
214+
*/
215+
ReactElement.dangerouslyTrustAllSources = function(acceptPossibleXSSHoles) {
216+
trustAllSources =
217+
acceptPossibleXSSHoles === undefined ? true : acceptPossibleXSSHoles;
218+
};
219+
149220
ReactElement.createElement = function(type, config, children) {
150221
var propName;
151222

@@ -298,7 +369,36 @@ ReactElement.isValidElement = function(object) {
298369
// same time. This will screw with ownership and stuff. Fix it, please.
299370
// TODO: We could possibly warn here.
300371
// }
301-
return isElement;
372+
if (!isElement) {
373+
return false;
374+
}
375+
376+
var sourceID = object && object._source;
377+
378+
// TODO: remove in a future version when security is on by default and
379+
// trustAllSources is no longer a ?Boolean.
380+
if (__DEV__) {
381+
if (trustAllSources === null &&
382+
!hasWarnedAboutUntrustedSource &&
383+
!(sourceID && sourceID === ownSourceID)) {
384+
hasWarnedAboutUntrustedSource = true;
385+
warning(
386+
false,
387+
'React is rendering an element from an unknown or foreign source. ' +
388+
'This is potentially malicious and a future version of React will ' +
389+
'not render this element. Call ' +
390+
'React.dangerouslyTrustAllSources(false) to disable rendering from ' +
391+
'unknown and foriegn sources.'
392+
);
393+
}
394+
}
395+
396+
// Determine if we trust the source of this particular React Element.
397+
return trustAllSources !== false ||
398+
sourceID && (
399+
sourceID === ownSourceID ||
400+
trustedSourceIDs && trustedSourceIDs.hasOwnProperty(sourceID)
401+
);
302402
};
303403

304404
module.exports = ReactElement;
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Copyright 2013-2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @emails react-core
10+
*/
11+
12+
'use strict';
13+
14+
var assign = require('Object.assign');
15+
16+
var React;
17+
var ReactTestUtils;
18+
var Component;
19+
20+
function makeElement(type, props, source) {
21+
return {
22+
type: type,
23+
key: null,
24+
ref: null,
25+
props: props,
26+
_store: {props: props, originalProps: assign({}, props)},
27+
_source: source,
28+
_isReactElement: true
29+
};
30+
}
31+
32+
describe('ReactElementSource', function() {
33+
34+
beforeEach(function() {
35+
require('mock-modules').dumpCache();
36+
React = require('React');
37+
ReactTestUtils = require('ReactTestUtils');
38+
Component = React.createClass({
39+
render: function() {
40+
return <div>{this.props.element}</div>;
41+
}
42+
});
43+
});
44+
45+
// TODO: this test is removed when warnings are removed in a future version.
46+
it('should not warn when rendering an known element', function () {
47+
spyOn(console, 'error');
48+
49+
var element = <div className="self">Component</div>;
50+
var component = ReactTestUtils.renderIntoDocument(
51+
<Component element={element} />
52+
);
53+
54+
expect(console.error.calls.length).toBe(0);
55+
});
56+
57+
// TODO: this test is removed when warnings are removed in a future version.
58+
it('should warn when rendering an unknown element', function () {
59+
spyOn(console, 'error');
60+
61+
var element = makeElement('div', {className: 'unknown'}, undefined);
62+
var component = ReactTestUtils.renderIntoDocument(
63+
<Component element={element} />
64+
);
65+
expect(React.findDOMNode(component).childNodes[0].className).toBe('unknown');
66+
expect(console.error.calls[0].args[0]).toBe(
67+
'Warning: ' +
68+
'React is rendering an element from an unknown or foreign source. ' +
69+
'This is potentially malicious and a future version of React will ' +
70+
'not render this element. Call ' +
71+
'React.dangerouslyTrustAllSources(false) to disable rendering from ' +
72+
'unknown and foriegn sources.'
73+
);
74+
});
75+
76+
// TODO: this test is removed when warnings are removed in a future version.
77+
it('should warn when rendering an foreign element', function () {
78+
spyOn(console, 'error');
79+
80+
var element = makeElement('div', {className: 'foreign'}, 'randomstring');
81+
var component = ReactTestUtils.renderIntoDocument(
82+
<Component element={element} />
83+
);
84+
expect(React.findDOMNode(component).childNodes[0].className).toBe('foreign');
85+
expect(console.error.calls[0].args[0]).toBe(
86+
'Warning: ' +
87+
'React is rendering an element from an unknown or foreign source. ' +
88+
'This is potentially malicious and a future version of React will ' +
89+
'not render this element. Call ' +
90+
'React.dangerouslyTrustAllSources(false) to disable rendering from ' +
91+
'unknown and foriegn sources.'
92+
);
93+
});
94+
95+
it('should render an element created by itself', function() {
96+
spyOn(console, 'error');
97+
React.dangerouslyTrustAllSources(false);
98+
99+
var element = <div className="self">Component</div>;
100+
expect(element._source).not.toBe(undefined);
101+
var component = ReactTestUtils.renderIntoDocument(
102+
<Component element={element} />
103+
);
104+
expect(React.findDOMNode(component).childNodes[0].className).toBe('self');
105+
expect(console.error.calls.length).toBe(0);
106+
});
107+
108+
it('should not render an unknown element', function() {
109+
spyOn(console, 'error');
110+
React.dangerouslyTrustAllSources(false);
111+
112+
var element = makeElement('div', {className: 'unknown'}, undefined);
113+
var component = ReactTestUtils.renderIntoDocument(
114+
<Component element={element} />
115+
);
116+
expect(React.findDOMNode(component).childNodes[0].className).not.toBe('unknown');
117+
expect(console.error.calls[0].args[0]).toBe(
118+
'Warning: Any use of a keyed object should be wrapped in ' +
119+
'React.addons.createFragment(object) before being passed as a child.'
120+
);
121+
});
122+
123+
it('should render an element created by a trusted source', function() {
124+
spyOn(console, 'error');
125+
React.trustSource('randomstring');
126+
127+
var element = makeElement('div', {className: 'trusted'}, 'randomstring');
128+
var component = ReactTestUtils.renderIntoDocument(
129+
<Component element={element} />
130+
);
131+
expect(React.findDOMNode(component).childNodes[0].className).toBe('trusted');
132+
expect(console.error.calls.length).toBe(0);
133+
});
134+
135+
it('should not render an element created by an foreign source', function() {
136+
spyOn(console, 'error');
137+
React.trustSource('randomstring');
138+
139+
var element = makeElement('div', {className: 'foreign'}, 'differentrandomstring');
140+
var component = ReactTestUtils.renderIntoDocument(
141+
<Component element={element} />
142+
);
143+
expect(React.findDOMNode(component).childNodes[0].className).not.toBe('foreign');
144+
expect(console.error.calls[0].args[0]).toBe(
145+
'Warning: Any use of a keyed object should be wrapped in ' +
146+
'React.addons.createFragment(object) before being passed as a child.'
147+
);
148+
});
149+
150+
it('should render unknown element when dangerously trusting', function() {
151+
spyOn(console, 'error');
152+
React.dangerouslyTrustAllSources();
153+
154+
var element = makeElement('div', {className: 'unknown'}, undefined);
155+
var component = ReactTestUtils.renderIntoDocument(
156+
<Component element={element} />
157+
);
158+
expect(React.findDOMNode(component).childNodes[0].className).toBe('unknown');
159+
expect(console.error.calls.length).toBe(0);
160+
});
161+
162+
it('should render foreign element when dangerously trusting', function() {
163+
spyOn(console, 'error');
164+
React.dangerouslyTrustAllSources();
165+
166+
var element = makeElement('div', {className: 'foreign'}, 'randomforeignstring');
167+
var component = ReactTestUtils.renderIntoDocument(
168+
<Component element={element} />
169+
);
170+
expect(React.findDOMNode(component).childNodes[0].className).toBe('foreign');
171+
expect(console.error.calls.length).toBe(0);
172+
});
173+
174+
});

0 commit comments

Comments
 (0)