Skip to content

Commit 7a00239

Browse files
committed
Merge pull request #4832 from sebmarkbage/xssfix
Use a Symbol to tag every ReactElement
2 parents a05691f + 031fc24 commit 7a00239

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)