Add trusted types to react on client side (#16157)

* Add trusted types to react on client side

* Implement changes according to review

* Remove support for trusted URLs, change TrustedTypes to trustedTypes

* Add support for deprecated trusted URLs

* Apply PR suggesstions

* Warn only once, remove forgotten check, put it behind a flag

* Move comment

* Fix PR comments

* Fix html toString concatenation

* Fix forgotten else branch

* Fix PR comments
This commit is contained in:
Emanuel Tesař
2019-09-16 14:43:22 +02:00
committed by Dan Abramov
parent cdbfa5044b
commit b8d079b413
18 changed files with 259 additions and 19 deletions

View File

@@ -149,5 +149,6 @@ module.exports = {
spyOnProd: true,
__PROFILE__: true,
__UMD__: true,
trustedTypes: true,
},
};

View File

@@ -16,7 +16,9 @@ import {
OVERLOADED_BOOLEAN,
} from '../shared/DOMProperty';
import sanitizeURL from '../shared/sanitizeURL';
import {toStringOrTrustedType} from './ToStringValue';
import {disableJavaScriptURLs} from 'shared/ReactFeatureFlags';
import {setAttribute, setAttributeNS} from './setAttribute';
import type {PropertyInfo} from '../shared/DOMProperty';
@@ -142,7 +144,7 @@ export function setValueForProperty(
if (value === null) {
node.removeAttribute(attributeName);
} else {
node.setAttribute(attributeName, '' + (value: any));
setAttribute(node, attributeName, toStringOrTrustedType(value));
}
}
return;
@@ -168,19 +170,21 @@ export function setValueForProperty(
const {type} = propertyInfo;
let attributeValue;
if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) {
// If attribute type is boolean, we know for sure it won't be an execution sink
// and we won't require Trusted Type here.
attributeValue = '';
} else {
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
attributeValue = '' + (value: any);
attributeValue = toStringOrTrustedType(value);
if (propertyInfo.sanitizeURL) {
sanitizeURL(attributeValue);
sanitizeURL(attributeValue.toString());
}
}
if (attributeNamespace) {
node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
setAttributeNS(node, attributeNamespace, attributeName, attributeValue);
} else {
node.setAttribute(attributeName, attributeValue);
setAttribute(node, attributeName, attributeValue);
}
}
}

View File

@@ -85,11 +85,16 @@ import possibleStandardNames from '../shared/possibleStandardNames';
import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
import {toStringOrTrustedType} from './ToStringValue';
import {enableFlareAPI} from 'shared/ReactFeatureFlags';
import {
enableFlareAPI,
enableTrustedTypesIntegration,
} from 'shared/ReactFeatureFlags';
let didWarnInvalidHydration = false;
let didWarnShadyDOM = false;
let didWarnScriptTags = false;
const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';
const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';
@@ -422,6 +427,18 @@ export function createElement(
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
const div = ownerDocument.createElement('div');
if (__DEV__) {
if (enableTrustedTypesIntegration && !didWarnScriptTags) {
warning(
false,
'Encountered a script tag while rendering React component. ' +
'Scripts inside React components are never executed when rendering ' +
'on the client. Consider using template tag instead ' +
'(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
);
didWarnScriptTags = true;
}
}
div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
// This is guaranteed to yield a script element.
const firstChild = ((div.firstChild: any): HTMLScriptElement);
@@ -776,7 +793,10 @@ export function diffProperties(
const lastHtml = lastProp ? lastProp[HTML] : undefined;
if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
(updatePayload = updatePayload || []).push(
propKey,
toStringOrTrustedType(nextHtml),
);
}
} else {
// TODO: It might be too late to clear this if we have children

View File

@@ -7,6 +7,8 @@
* @flow
*/
import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
export opaque type ToStringValue =
| boolean
| number
@@ -35,3 +37,45 @@ export function getToStringValue(value: mixed): ToStringValue {
return '';
}
}
/**
* Returns true only if Trusted Types are available in global object and the value is a trusted type.
*/
let isTrustedTypesValue: (value: any) => boolean;
// $FlowExpectedError - TrustedTypes are defined only in some browsers or with polyfill
if (enableTrustedTypesIntegration && typeof trustedTypes !== 'undefined') {
isTrustedTypesValue = (value: any) =>
trustedTypes.isHTML(value) ||
trustedTypes.isScript(value) ||
trustedTypes.isScriptURL(value) ||
// TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204
(trustedTypes.isURL && trustedTypes.isURL(value));
} else {
isTrustedTypesValue = () => false;
}
/** Trusted value is a wrapper for "safe" values which can be assigned to DOM execution sinks. */
export opaque type TrustedValue: {toString(): string, valueOf(): string} = {
toString(): string,
valueOf(): string,
};
/**
* We allow passing objects with toString method as element attributes or in dangerouslySetInnerHTML
* and we do validations that the value is safe. Once we do validation we want to use the validated
* value instead of the object (because object.toString may return something else on next call).
*
* If application uses Trusted Types we don't stringify trusted values, but preserve them as objects.
*/
export function toStringOrTrustedType(value: any): string | TrustedValue {
if (
enableTrustedTypesIntegration &&
// fast-path string values as it's most frequent usage of the function
typeof value !== 'string' &&
isTrustedTypesValue(value)
) {
return value;
} else {
return '' + value;
}
}

View File

@@ -0,0 +1,101 @@
describe('when Trusted Types are available in global object', () => {
let React;
let ReactDOM;
let ReactFeatureFlags;
let container;
beforeEach(() => {
container = document.createElement('div');
window.trustedTypes = {
isHTML: () => true,
isScript: () => false,
isScriptURL: () => false,
};
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableTrustedTypesIntegration = true;
React = require('react');
ReactDOM = require('react-dom');
});
afterEach(() => {
delete window.trustedTypes;
ReactFeatureFlags.enableTrustedTypesIntegration = false;
});
it('should not stringify trusted values', () => {
const trustedObject = {toString: () => 'I look like a trusted object'};
class Component extends React.Component {
state = {inner: undefined};
render() {
return <div dangerouslySetInnerHTML={{__html: this.state.inner}} />;
}
}
const isHTMLSpy = jest.spyOn(window.trustedTypes, ['isHTML']);
const instance = ReactDOM.render(<Component />, container);
instance.setState({inner: trustedObject});
expect(container.firstChild.innerHTML).toBe(trustedObject.toString());
expect(isHTMLSpy).toHaveBeenCalledWith(trustedObject);
});
describe('dangerouslySetInnerHTML in svg elements in Internet Explorer', () => {
let innerHTMLDescriptor;
// simulate svg elements in Internet Explorer which don't have 'innerHTML' property
beforeEach(() => {
innerHTMLDescriptor = Object.getOwnPropertyDescriptor(
Element.prototype,
'innerHTML',
);
delete Element.prototype.innerHTML;
Object.defineProperty(
HTMLDivElement.prototype,
'innerHTML',
innerHTMLDescriptor,
);
});
afterEach(() => {
delete HTMLDivElement.prototype.innerHTML;
Object.defineProperty(
Element.prototype,
'innerHTML',
innerHTMLDescriptor,
);
});
it('should log a warning', () => {
class Component extends React.Component {
render() {
return <svg dangerouslySetInnerHTML={{__html: 'unsafe html'}} />;
}
}
expect(() => {
ReactDOM.render(<Component />, container);
}).toWarnDev(
"Warning: Using 'dangerouslySetInnerHTML' in an svg element with " +
'Trusted Types enabled in an Internet Explorer will cause ' +
'the trusted value to be converted to string. Assigning string ' +
"to 'innerHTML' will throw an error if Trusted Types are enforced. " +
"You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " +
'on the enclosing div instead.',
);
});
});
it('should warn once when rendering script tag in jsx on client', () => {
expect(() => {
ReactDOM.render(<script>alert("I am not executed")</script>, container);
}).toWarnDev(
'Warning: Encountered a script tag while rendering React component. ' +
'Scripts inside React components are never executed when rendering ' +
'on the client. Consider using template tag instead ' +
'(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' +
' in script (at **)',
);
// check that the warning is print only once
ReactDOM.render(<script>alert("I am not executed")</script>, container);
});
});

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {TrustedValue} from './ToStringValue';
/**
* Set attribute for a node. The attribute value can be either string or
* Trusted value (if application uses Trusted Types).
*/
export function setAttribute(
node: Element,
attributeName: string,
attributeValue: string | TrustedValue,
) {
node.setAttribute(attributeName, (attributeValue: any));
}
/**
* Set attribute with namespace for a node. The attribute value can be either string or
* Trusted value (if application uses Trusted Types).
*/
export function setAttributeNS(
node: Element,
attributeNamespace: string,
attributeName: string,
attributeValue: string | TrustedValue,
) {
node.setAttributeNS(attributeNamespace, attributeName, (attributeValue: any));
}

View File

@@ -9,6 +9,9 @@
import {Namespaces} from '../shared/DOMNamespaces';
import createMicrosoftUnsafeLocalFunction from '../shared/createMicrosoftUnsafeLocalFunction';
import warning from 'shared/warning';
import type {TrustedValue} from './ToStringValue';
import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
// SVG temp container for IE lacking innerHTML
let reusableSVGContainer;
@@ -22,25 +25,41 @@ let reusableSVGContainer;
*/
const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
node: Element,
html: string,
html: string | TrustedValue,
): void {
// IE does not have innerHTML for SVG nodes, so instead we inject the
// new markup in a temp node and then move the child nodes across into
// the target node
if (node.namespaceURI === Namespaces.svg && !('innerHTML' in node)) {
reusableSVGContainer =
reusableSVGContainer || document.createElement('div');
reusableSVGContainer.innerHTML = '<svg>' + html + '</svg>';
const svgNode = reusableSVGContainer.firstChild;
while (node.firstChild) {
node.removeChild(node.firstChild);
if (node.namespaceURI === Namespaces.svg) {
if (enableTrustedTypesIntegration && __DEV__) {
warning(
// $FlowExpectedError - trustedTypes are defined only in some browsers or with polyfill
typeof trustedTypes === 'undefined',
"Using 'dangerouslySetInnerHTML' in an svg element with " +
'Trusted Types enabled in an Internet Explorer will cause ' +
'the trusted value to be converted to string. Assigning string ' +
"to 'innerHTML' will throw an error if Trusted Types are enforced. " +
"You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " +
'on the enclosing div instead.',
);
}
while (svgNode.firstChild) {
node.appendChild(svgNode.firstChild);
if (!('innerHTML' in node)) {
reusableSVGContainer =
reusableSVGContainer || document.createElement('div');
reusableSVGContainer.innerHTML =
'<svg>' + html.valueOf().toString() + '</svg>';
const svgNode = reusableSVGContainer.firstChild;
while (node.firstChild) {
node.removeChild(node.firstChild);
}
while (svgNode.firstChild) {
node.appendChild(svgNode.firstChild);
}
} else {
node.innerHTML = (html: any);
}
} else {
node.innerHTML = html;
node.innerHTML = (html: any);
}
});

View File

@@ -100,3 +100,5 @@ export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;

View File

@@ -43,6 +43,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
// Only used in www builds.
export function addUserTimingListener() {

View File

@@ -38,6 +38,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
// Only used in www builds.
export function addUserTimingListener() {

View File

@@ -38,6 +38,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
// Only used in www builds.
export function addUserTimingListener() {

View File

@@ -38,6 +38,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
// Only used in www builds.
export function addUserTimingListener() {

View File

@@ -36,6 +36,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
// Only used in www builds.
export function addUserTimingListener() {

View File

@@ -22,6 +22,7 @@ export const {
enableUserBlockingEvents,
disableLegacyContext,
disableSchedulerTimeoutBasedOnReactExpirationTime,
enableTrustedTypesIntegration,
warnAboutStringRefs,
warnAboutDefaultPropsOnFunctionComponents,
} = require('ReactFeatureFlags');

View File

@@ -21,6 +21,8 @@ module.exports = {
process: true,
setImmediate: true,
Buffer: true,
// Trusted Types
trustedTypes: true,
// Scheduler profiling
SharedArrayBuffer: true,

View File

@@ -22,6 +22,8 @@ module.exports = {
// Node.js Server Rendering
setImmediate: true,
Buffer: true,
// Trusted Types
trustedTypes: true,
// Scheduler profiling
SharedArrayBuffer: true,

View File

@@ -21,6 +21,8 @@ module.exports = {
// Fabric. See https://github.com/facebook/react/pull/15490
// for more information
nativeFabricUIManager: true,
// Trusted Types
trustedTypes: true,
// Scheduler profiling
SharedArrayBuffer: true,

View File

@@ -24,6 +24,8 @@ module.exports = {
define: true,
require: true,
global: true,
// Trusted Types
trustedTypes: true,
// Scheduler profiling
SharedArrayBuffer: true,