Skip to content

Commit 031fc24

Browse files
committed
Use a Symbol to tag every ReactElement
Fixes #3473 I tag each React element with `$$typeof: Symbol.for('react.element')`. We need this to be able to safely distinguish these from plain objects that might have come from user provided JSON. The idiomatic JavaScript way of tagging an object is for it to inherent some prototype and then use `instanceof` to test for it. However, this has limitations since it doesn't work with value types which require `typeof` checks. They also don't work across realms. Which is why there are alternative tag checks like `Array.isArray` or the `toStringTag`. Another problem is that different instances of React that might have been created not knowing about eachother. npm tends to make this kind of problem occur a lot. Additionally, it is our hope that ReactElement will one day be specified in terms of a "Value Type" style record instead of a plain Object. This Value Types proposal by @nikomatsakis is currently on hold but does satisfy all these requirements: https://github.com/nikomatsakis/typed-objects-explainer/blob/master/valuetypes.md#the-typeof-operator Additionally, there is already a system for coordinating tags across module systems and even realms in ES6. Namely using `Symbol.for`. Currently these objects are not able to transfer between Workers but there is nothing preventing that from being possible in the future. You could imagine even `Symbol.for` working across Worker boundaries. You could also build a system that coordinates Symbols and Value Types from server to client or through serialized forms. That's beyond the scope of React itself, and if it was built it seems like it would belong with the `Symbol` system. A system could override the `Symbol.for('react.element')` to return a plain yet cryptographically random or unique number. That would allow ReactElements to pass through JSON without risking the XSS issue. The fallback solution is a plain well-known number. This makes it unsafe with regard to the XSS issue described in #3473. We could have used a much more convoluted solution to protect against JSON specifically but that would require some kind of significant coordination, or change the check to do a `typeof element.$$typeof === 'function'` check which would not make it unique to React. It seems cleaner to just use a fixed number since the protection is just a secondary layer anyway. I'm not sure if this is the right tradeoff. In short, if you want the XSS protection, use a proper Symbol polyfill. Finally, the reason for calling it `$$typeof` is to avoid confusion with `.type` and the use case is to add a tag that the `typeof` operator would refer to. I would use `@@typeof` but that seems to deopt in JSC. I also don't use `__typeof` because this is more than a framework private. It should really be part of the polyfilling layer.
1 parent a05691f commit 031fc24

File tree

3 files changed

+93
-16
lines changed

3 files changed

+93
-16
lines changed

src/isomorphic/classic/element/ReactElement.js

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ var ReactCurrentOwner = require('ReactCurrentOwner');
1515

1616
var assign = require('Object.assign');
1717

18+
// The Symbol used to tag the ReactElement type. If there is no native Symbol
19+
// nor polyfill, then a plain number is used for performance.
20+
var TYPE_SYMBOL = (typeof Symbol === 'function' && Symbol.for &&
21+
Symbol.for('react.element')) || 0xeac7;
22+
1823
var RESERVED_PROPS = {
1924
key: true,
2025
ref: true,
@@ -52,17 +57,17 @@ if (__DEV__) {
5257
*/
5358
var ReactElement = function(type, key, ref, self, source, owner, props) {
5459
var element = {
60+
// This tag allow us to uniquely identify this as a React Element
61+
$$typeof: TYPE_SYMBOL,
62+
5563
// Built-in properties that belong on the element
5664
type: type,
5765
key: key,
5866
ref: ref,
59-
self: self,
60-
source: source,
67+
props: props,
6168

6269
// Record the component responsible for creating this element.
6370
_owner: owner,
64-
65-
props: props,
6671
};
6772

6873
if (__DEV__) {
@@ -83,8 +88,25 @@ var ReactElement = function(type, key, ref, self, source, owner, props) {
8388
writable: true,
8489
value: false,
8590
});
91+
// self and source are DEV only properties.
92+
Object.defineProperty(element, '_self', {
93+
configurable: false,
94+
enumerable: false,
95+
writable: false,
96+
value: self,
97+
});
98+
// Two elements created in two different places should be considered
99+
// equal for testing purposes and therefore we hide it from enumeration.
100+
Object.defineProperty(element, '_source', {
101+
configurable: false,
102+
enumerable: false,
103+
writable: false,
104+
value: source,
105+
});
86106
} else {
87-
this._store.validated = false;
107+
element._store.validated = false;
108+
element._self = self;
109+
element._source = source;
88110
}
89111
Object.freeze(element.props);
90112
Object.freeze(element);
@@ -164,12 +186,12 @@ ReactElement.createFactory = function(type) {
164186
};
165187

166188
ReactElement.cloneAndReplaceKey = function(oldElement, newKey) {
167-
var newElement = new ReactElement(
189+
var newElement = ReactElement(
168190
oldElement.type,
169191
newKey,
170192
oldElement.ref,
171-
oldElement.self,
172-
oldElement.source,
193+
oldElement._self,
194+
oldElement._source,
173195
oldElement._owner,
174196
oldElement.props
175197
);
@@ -182,8 +204,8 @@ ReactElement.cloneAndReplaceProps = function(oldElement, newProps) {
182204
oldElement.type,
183205
oldElement.key,
184206
oldElement.ref,
185-
oldElement.self,
186-
oldElement.source,
207+
oldElement._self,
208+
oldElement._source,
187209
oldElement._owner,
188210
newProps
189211
);
@@ -205,8 +227,12 @@ ReactElement.cloneElement = function(element, config, children) {
205227
// Reserved names are extracted
206228
var key = element.key;
207229
var ref = element.ref;
208-
var self = element.__self;
209-
var source = element.__source;
230+
// Self is preserved since the owner is preserved.
231+
var self = element._self;
232+
// Source is preserved since cloneElement is unlikely to be targeted by a
233+
// transpiler, and the original source is probably a better indicator of the
234+
// true owner.
235+
var source = element._source;
210236

211237
// Owner will be preserved, unless ref is overridden
212238
var owner = element._owner;
@@ -259,11 +285,10 @@ ReactElement.cloneElement = function(element, config, children) {
259285
* @final
260286
*/
261287
ReactElement.isValidElement = function(object) {
262-
return !!(
288+
return (
263289
typeof object === 'object' &&
264-
object != null &&
265-
'type' in object &&
266-
'props' in object
290+
object !== null &&
291+
object.$$typeof === TYPE_SYMBOL
267292
);
268293
};
269294

src/isomorphic/classic/element/__tests__/ReactElement-test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ describe('ReactElement', function() {
2424
beforeEach(function() {
2525
require('mock-modules').dumpCache();
2626

27+
// Delete the native Symbol if we have one to ensure we test the
28+
// unpolyfilled environment.
29+
delete global.Symbol;
30+
2731
React = require('React');
2832
ReactDOM = require('ReactDOM');
2933
ReactTestUtils = require('ReactTestUtils');
@@ -190,6 +194,10 @@ describe('ReactElement', function() {
190194
expect(React.isValidElement('string')).toEqual(false);
191195
expect(React.isValidElement(React.DOM.div)).toEqual(false);
192196
expect(React.isValidElement(Component)).toEqual(false);
197+
expect(React.isValidElement({ type: 'div', props: {} })).toEqual(false);
198+
199+
var jsonElement = JSON.stringify(React.createElement('div'));
200+
expect(React.isValidElement(JSON.parse(jsonElement))).toBe(true);
193201
});
194202

195203
it('allows the use of PropTypes validators in statics', function() {
@@ -305,4 +313,47 @@ describe('ReactElement', function() {
305313
expect(console.error.argsForCall.length).toBe(0);
306314
});
307315

316+
it('identifies elements, but not JSON, if Symbols are supported', function() {
317+
// Rudimentary polyfill
318+
// Once all jest engines support Symbols natively we can swap this to test
319+
// WITH native Symbols by default.
320+
var TYPE_SYMBOL = function() {}; // fake Symbol
321+
var OTHER_SYMBOL = function() {}; // another fake Symbol
322+
global.Symbol = function(name) {
323+
return OTHER_SYMBOL;
324+
};
325+
global.Symbol.for = function(key) {
326+
if (key === 'react.element') {
327+
return TYPE_SYMBOL;
328+
}
329+
return OTHER_SYMBOL;
330+
};
331+
332+
require('mock-modules').dumpCache();
333+
334+
React = require('React');
335+
336+
var Component = React.createClass({
337+
render: function() {
338+
return React.createElement('div');
339+
},
340+
});
341+
342+
expect(React.isValidElement(React.createElement('div')))
343+
.toEqual(true);
344+
expect(React.isValidElement(React.createElement(Component)))
345+
.toEqual(true);
346+
347+
expect(React.isValidElement(null)).toEqual(false);
348+
expect(React.isValidElement(true)).toEqual(false);
349+
expect(React.isValidElement({})).toEqual(false);
350+
expect(React.isValidElement('string')).toEqual(false);
351+
expect(React.isValidElement(React.DOM.div)).toEqual(false);
352+
expect(React.isValidElement(Component)).toEqual(false);
353+
expect(React.isValidElement({ type: 'div', props: {} })).toEqual(false);
354+
355+
var jsonElement = JSON.stringify(React.createElement('div'));
356+
expect(React.isValidElement(JSON.parse(jsonElement))).toBe(false);
357+
});
358+
308359
});

src/isomorphic/modern/element/__tests__/ReactJSXElement-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ describe('ReactJSXElement', function() {
153153
expect(React.isValidElement({})).toEqual(false);
154154
expect(React.isValidElement('string')).toEqual(false);
155155
expect(React.isValidElement(Component)).toEqual(false);
156+
expect(React.isValidElement({ type: 'div', props: {} })).toEqual(false);
156157
});
157158

158159
it('is indistinguishable from a plain object', function() {

0 commit comments

Comments
 (0)