Skip to content
9 changes: 6 additions & 3 deletions packages/react-dom/src/client/DOMPropertyOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
OVERLOADED_BOOLEAN,
} from '../shared/DOMProperty';
import sanitizeURL from '../shared/sanitizeURL';
import trustedTypesAwareToString from '../shared/trustedTypesAwareToString';
import {disableJavaScriptURLs} from 'shared/ReactFeatureFlags';

import type {PropertyInfo} from '../shared/DOMProperty';
Expand Down Expand Up @@ -142,7 +143,7 @@ export function setValueForProperty(
if (value === null) {
node.removeAttribute(attributeName);
} else {
node.setAttribute(attributeName, '' + (value: any));
node.setAttribute(attributeName, trustedTypesAwareToString(value));
}
}
return;
Expand All @@ -168,13 +169,15 @@ 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 = trustedTypesAwareToString(value);
if (propertyInfo.sanitizeURL) {
sanitizeURL(attributeValue);
sanitizeURL('' + attributeValue);
}
}
if (attributeNamespace) {
Expand Down
16 changes: 6 additions & 10 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ 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 trustedTypesAwareToString from '../shared/trustedTypesAwareToString';

import {enableFlareAPI} from 'shared/ReactFeatureFlags';

Expand Down Expand Up @@ -416,15 +417,7 @@ export function createElement(
);
}

if (type === 'script') {
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
const div = ownerDocument.createElement('div');
div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
// This is guaranteed to yield a script element.
const firstChild = ((div.firstChild: any): HTMLScriptElement);
domElement = div.removeChild(firstChild);
} else if (typeof props.is === 'string') {
if (typeof props.is === 'string') {
// $FlowIssue `createElement` should be updated for Web Components
domElement = ownerDocument.createElement(type, {is: props.is});
} else {
Expand Down Expand Up @@ -773,7 +766,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,
trustedTypesAwareToString(nextHtml),
);
}
} else {
// TODO: It might be too late to clear this if we have children
Expand Down
3 changes: 2 additions & 1 deletion packages/react-dom/src/client/ToStringValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*
* @flow
*/
import trustedTypesAwareToString from '../shared/trustedTypesAwareToString';

export opaque type ToStringValue =
| boolean
Expand All @@ -19,7 +20,7 @@ export opaque type ToStringValue =
// around this limitation, we use an opaque type that can only be obtained by
// passing the value through getToStringValue first.
export function toString(value: ToStringValue): string {
return '' + (value: any);
return trustedTypesAwareToString(value);
}

export function getToStringValue(value: mixed): ToStringValue {
Expand Down
85 changes: 85 additions & 0 deletions packages/react-dom/src/client/__tests__/trustedTypes-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
describe('when Trusted Types are available in global object', () => {
let React;
let ReactDOM;

beforeEach(() => {
React = require('react');
ReactDOM = require('react-dom');
window.TrustedTypes = {
isHTML: () => true,
isScript: () => false,
isScriptURL: () => false,
isURL: () => false,
};
});

it('should not stringify trusted values', () => {
const container = document.createElement('div');
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 instace = ReactDOM.render(<Component />, container);
instace.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 {
state = {inner: undefined};
render() {
return <svg dangerouslySetInnerHTML={{__html: this.state.inner}} />;
}
}
const errorSpy = spyOnDev(console, 'error');

const container = document.createElement('div');
const instace = ReactDOM.render(<Component />, container);
instace.setState({inner: 'anyValue'});

if (__DEV__) {
expect(errorSpy).toHaveBeenCalledWith(
"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.',
);
}
});
});
});
13 changes: 12 additions & 1 deletion packages/react-dom/src/client/setInnerHTML.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import {Namespaces} from '../shared/DOMNamespaces';
import createMicrosoftUnsafeLocalFunction from '../shared/createMicrosoftUnsafeLocalFunction';
import warningWithoutStack from 'shared/warningWithoutStack';

// SVG temp container for IE lacking innerHTML
let reusableSVGContainer;
Expand All @@ -27,10 +28,20 @@ const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
// 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');
if (__DEV__) {
warningWithoutStack(
typeof window.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.',
);
}
reusableSVGContainer.innerHTML = '<svg>' + html + '</svg>';
const svgNode = reusableSVGContainer.firstChild;
while (node.firstChild) {
Expand Down
42 changes: 42 additions & 0 deletions packages/react-dom/src/shared/trustedTypesAwareToString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* 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
*/

/**
* Returns true only if Trusted Types are available in global object and the value
* is a trusted type.
*/
function isTrustedTypesValue(value: any) {
if (typeof window.TrustedTypes === 'undefined') {
return false;
} else {
return (
window.TrustedTypes.isHTML(value) ||
window.TrustedTypes.isScript(value) ||
window.TrustedTypes.isScriptURL(value) ||
window.TrustedTypes.isURL(value)
);
}
}

/**
* 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.
*/
function trustedTypesAwareToString(value: any) {
if (isTrustedTypesValue(value)) {
return value;
} else {
return '' + value;
}
}

export default trustedTypesAwareToString;