Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Trusted sources for React elements. #3583

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/browser/ui/React.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ var React = {
unmountComponentAtNode: ReactMount.unmountComponentAtNode,
isValidElement: ReactElement.isValidElement,
withContext: ReactContext.withContext,
getSourceID: ReactElement.getSourceID,
trustSource: ReactElement.trustSource,
dangerouslyTrustAllSources: ReactElement.dangerouslyTrustAllSources,

// Hook for JSX spread, don't use this for anything else.
__spread: assign
Expand Down
102 changes: 101 additions & 1 deletion src/classic/element/ReactElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,35 @@ function defineWarningProperty(object, key) {
*/
var useMutationMembrane = false;

/**
* React instances own source ID is a randomly generated string. It does not
* have to be globally unique, just difficult to guess.
*/
var ownSourceID =
(typeof crypto === 'object' && crypto.getRandomValues ?
crypto.getRandomValues(new Uint32Array(1))[0] :
~(Math.random() * (1 << 31))
).toString(36);

/**
* If trustSource() is called, becomes an mapping of sourceIDs we trust as
* valid React Elements.
*/
var trustedSourceIDs; // ?{ [sourceID: string]: true }

/**
* If true, dangerously trust all sources. If false, only trust explicit
* sources.
*
* If null, trust all sources but warn if not explicitly trusted.
*/
var trustAllSources = null; // ?Boolean

/**
* Updated to true if a warning is logged so we don't spam console.
*/
var hasWarnedAboutUntrustedSource;

/**
* Warn for mutations.
*
Expand Down Expand Up @@ -139,13 +168,55 @@ var ReactElement = function(type, key, ref, owner, context, props) {
// We intentionally don't expose the function on the constructor property.
// ReactElement should be indistinguishable from a plain object.
ReactElement.prototype = {
_source: ownSourceID,
_isReactElement: true
};

if (__DEV__) {
defineMutationMembrane(ReactElement.prototype);
}


/**
* Return this React module's source ID.
*/
ReactElement.getSourceID = function() {
return ownSourceID;
};

/**
* Allows this React module to trust React elements produced from another React
* module, potentially from a Server or from another Realm (iframe, webworker).
*/
ReactElement.trustSource = function(sourceID) {
if (!trustedSourceIDs) {
trustedSourceIDs = {};
}
trustedSourceIDs[sourceID] = true;
// Calling trustSource implies using React's trusted source.
// TODO: remove in a future version when security is on by default and
// trustAllSources is no longer a ?Boolean.
if (trustAllSources === null) {
trustAllSources = false;
}
};

/**
* Trust React elements regardless of their source, or even if they have an
* unknown source. Using this method may be helpful during a refactor to support
* React's security model, but should be avoided as it could allow XSS attacks
* in certain conditions.
*
* For backwards compatibility, the default behavior is to trust all sources,
* but to warn in __DEV__ when rendering a React component from an unknown
* source. In a future version of React only explicitly trusted sources may
* provide React components.
*/
ReactElement.dangerouslyTrustAllSources = function(acceptPossibleXSSHoles) {
trustAllSources =
acceptPossibleXSSHoles === undefined ? true : acceptPossibleXSSHoles;
};

ReactElement.createElement = function(type, config, children) {
var propName;

Expand Down Expand Up @@ -298,7 +369,36 @@ ReactElement.isValidElement = function(object) {
// same time. This will screw with ownership and stuff. Fix it, please.
// TODO: We could possibly warn here.
// }
return isElement;
if (!isElement) {
return false;
}

var sourceID = object && object._source;

// TODO: remove in a future version when security is on by default and
// trustAllSources is no longer a ?Boolean.
if (__DEV__) {
if (trustAllSources === null &&
!hasWarnedAboutUntrustedSource &&
!(sourceID && sourceID === ownSourceID)) {
hasWarnedAboutUntrustedSource = true;
warning(
false,
'React is rendering an element from an unknown or foreign source. ' +
'This is potentially malicious and a future version of React will ' +
'not render this element. Call ' +
'React.dangerouslyTrustAllSources(false) to disable rendering from ' +
'unknown and foriegn sources.'
);
}
}

// Determine if we trust the source of this particular React Element.
return trustAllSources !== false ||
sourceID && (
sourceID === ownSourceID ||
trustedSourceIDs && trustedSourceIDs.hasOwnProperty(sourceID)
);
};

module.exports = ReactElement;
174 changes: 174 additions & 0 deletions src/classic/element/__tests__/ReactElementSource-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/

'use strict';

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

var React;
var ReactTestUtils;
var Component;

function makeElement(type, props, source) {
return {
type: type,
key: null,
ref: null,
props: props,
_store: {props: props, originalProps: assign({}, props)},
_source: source,
_isReactElement: true
};
}

describe('ReactElementSource', function() {

beforeEach(function() {
require('mock-modules').dumpCache();
React = require('React');
ReactTestUtils = require('ReactTestUtils');
Component = React.createClass({
render: function() {
return <div>{this.props.element}</div>;
}
});
});

// TODO: this test is removed when warnings are removed in a future version.
it('should not warn when rendering an known element', function () {
spyOn(console, 'error');

var element = <div className="self">Component</div>;
var component = ReactTestUtils.renderIntoDocument(
<Component element={element} />
);

expect(console.error.calls.length).toBe(0);
});

// TODO: this test is removed when warnings are removed in a future version.
it('should warn when rendering an unknown element', function () {
spyOn(console, 'error');

var element = makeElement('div', {className: 'unknown'}, undefined);
var component = ReactTestUtils.renderIntoDocument(
<Component element={element} />
);
expect(React.findDOMNode(component).childNodes[0].className).toBe('unknown');
expect(console.error.calls[0].args[0]).toBe(
'Warning: ' +
'React is rendering an element from an unknown or foreign source. ' +
'This is potentially malicious and a future version of React will ' +
'not render this element. Call ' +
'React.dangerouslyTrustAllSources(false) to disable rendering from ' +
'unknown and foriegn sources.'
);
});

// TODO: this test is removed when warnings are removed in a future version.
it('should warn when rendering an foreign element', function () {
spyOn(console, 'error');

var element = makeElement('div', {className: 'foreign'}, 'randomstring');
var component = ReactTestUtils.renderIntoDocument(
<Component element={element} />
);
expect(React.findDOMNode(component).childNodes[0].className).toBe('foreign');
expect(console.error.calls[0].args[0]).toBe(
'Warning: ' +
'React is rendering an element from an unknown or foreign source. ' +
'This is potentially malicious and a future version of React will ' +
'not render this element. Call ' +
'React.dangerouslyTrustAllSources(false) to disable rendering from ' +
'unknown and foriegn sources.'
);
});

it('should render an element created by itself', function() {
spyOn(console, 'error');
React.dangerouslyTrustAllSources(false);

var element = <div className="self">Component</div>;
expect(element._source).not.toBe(undefined);
var component = ReactTestUtils.renderIntoDocument(
<Component element={element} />
);
expect(React.findDOMNode(component).childNodes[0].className).toBe('self');
expect(console.error.calls.length).toBe(0);
});

it('should not render an unknown element', function() {
spyOn(console, 'error');
React.dangerouslyTrustAllSources(false);

var element = makeElement('div', {className: 'unknown'}, undefined);
var component = ReactTestUtils.renderIntoDocument(
<Component element={element} />
);
expect(React.findDOMNode(component).childNodes[0].className).not.toBe('unknown');
expect(console.error.calls[0].args[0]).toBe(
'Warning: Any use of a keyed object should be wrapped in ' +
'React.addons.createFragment(object) before being passed as a child.'
);
});

it('should render an element created by a trusted source', function() {
spyOn(console, 'error');
React.trustSource('randomstring');

var element = makeElement('div', {className: 'trusted'}, 'randomstring');
var component = ReactTestUtils.renderIntoDocument(
<Component element={element} />
);
expect(React.findDOMNode(component).childNodes[0].className).toBe('trusted');
expect(console.error.calls.length).toBe(0);
});

it('should not render an element created by an foreign source', function() {
spyOn(console, 'error');
React.trustSource('randomstring');

var element = makeElement('div', {className: 'foreign'}, 'differentrandomstring');
var component = ReactTestUtils.renderIntoDocument(
<Component element={element} />
);
expect(React.findDOMNode(component).childNodes[0].className).not.toBe('foreign');
expect(console.error.calls[0].args[0]).toBe(
'Warning: Any use of a keyed object should be wrapped in ' +
'React.addons.createFragment(object) before being passed as a child.'
);
});

it('should render unknown element when dangerously trusting', function() {
spyOn(console, 'error');
React.dangerouslyTrustAllSources();

var element = makeElement('div', {className: 'unknown'}, undefined);
var component = ReactTestUtils.renderIntoDocument(
<Component element={element} />
);
expect(React.findDOMNode(component).childNodes[0].className).toBe('unknown');
expect(console.error.calls.length).toBe(0);
});

it('should render foreign element when dangerously trusting', function() {
spyOn(console, 'error');
React.dangerouslyTrustAllSources();

var element = makeElement('div', {className: 'foreign'}, 'randomforeignstring');
var component = ReactTestUtils.renderIntoDocument(
<Component element={element} />
);
expect(React.findDOMNode(component).childNodes[0].className).toBe('foreign');
expect(console.error.calls.length).toBe(0);
});

});