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

Add trusted types to react on server side #16555

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add trusted types to React for server side
  • Loading branch information
Emanuel Tesar committed Aug 21, 2019
commit 5536ccb1a2e8a17a4ddad99b72bb7e70de02a0e8
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,6 @@ module.exports = {
spyOnProd: true,
__PROFILE__: true,
__UMD__: true,
TrustedTypes: true,
},
};
37 changes: 34 additions & 3 deletions packages/react-dom/src/server/DOMMarkupOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*
* @flow
*/
import type {TrustedTypes} from './trustedTypes';

import {
ID_ATTRIBUTE_NAME,
Expand Down Expand Up @@ -41,18 +42,48 @@ export function createMarkupForRoot(): string {
/**
* Creates markup for a property.
*
* @param {string} name
* @param {*} value
* @param {string} name property name
* @param {*} value property value
* @param {string} tagLowercase lowercase tag name of the target element
* @param {TrustedTypes} trustedTypes Trusted Types implementation, which if provided enforces trusted types on server
* @return {?string} Markup string, or null if the property was invalid.
*/
export function createMarkupForProperty(name: string, value: mixed): string {
export function createMarkupForProperty(
name: string,
value: mixed,
tagLowercase: string,
trustedTypes: ?TrustedTypes,
): string {
const propertyInfo = getPropertyInfo(name);
if (name !== 'style' && shouldIgnoreAttribute(name, propertyInfo, false)) {
return '';
}
if (shouldRemoveAttribute(name, value, propertyInfo, false)) {
return '';
}
if (
trustedTypes &&
// TODO: getPropertyType is not yet implemented everywhere.
// once Trusted Types are stable remove this check.
trustedTypes.getPropertyType &&
trustedTypes.getPropertyType(tagLowercase, name)
) {
const requiredTrustedType = trustedTypes.getPropertyType(
tagLowercase,
name,
);
if (
(requiredTrustedType === 'TrustedHTML' && !trustedTypes.isHTML(value)) ||
(requiredTrustedType === 'TrustedScriptURL' &&
!trustedTypes.isScriptURL(value)) ||
(requiredTrustedType === 'TrustedURL' && !trustedTypes.isURL(value)) ||
(requiredTrustedType === 'TrustedScript' && !trustedTypes.isScript(value))
) {
throw new Error(
`${name} requires ${requiredTrustedType}! Received: ${(value: any)}`,
);
}
}
if (propertyInfo !== null) {
const attributeName = propertyInfo.attributeName;
const {type} = propertyInfo;
Expand Down
16 changes: 10 additions & 6 deletions packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import ReactPartialRenderer from './ReactPartialRenderer';

// This is a Readable Node.js stream which wraps the ReactDOMPartialRenderer.
class ReactMarkupReadableStream extends Readable {
constructor(element, makeStaticMarkup) {
constructor(element, makeStaticMarkup, trustedTypes) {
// Calls the stream.Readable(options) constructor. Consider exposing built-in
// features like highWaterMark in the future.
super({});
this.partialRenderer = new ReactPartialRenderer(element, makeStaticMarkup);
this.partialRenderer = new ReactPartialRenderer(
element,
makeStaticMarkup,
trustedTypes,
);
}

_destroy(err, callback) {
Expand All @@ -36,15 +40,15 @@ class ReactMarkupReadableStream extends Readable {
* server.
* See https://reactjs.org/docs/react-dom-server.html#rendertonodestream
*/
export function renderToNodeStream(element) {
return new ReactMarkupReadableStream(element, false);
export function renderToNodeStream(element, trustedTypes) {
return new ReactMarkupReadableStream(element, false, trustedTypes);
}

/**
* Similar to renderToNodeStream, except this doesn't create extra DOM attributes
* such as data-react-id that React uses internally.
* See https://reactjs.org/docs/react-dom-server.html#rendertostaticnodestream
*/
export function renderToStaticNodeStream(element) {
return new ReactMarkupReadableStream(element, true);
export function renderToStaticNodeStream(element, trustedTypes) {
return new ReactMarkupReadableStream(element, true, trustedTypes);
}
8 changes: 4 additions & 4 deletions packages/react-dom/src/server/ReactDOMStringRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import ReactPartialRenderer from './ReactPartialRenderer';
* server.
* See https://reactjs.org/docs/react-dom-server.html#rendertostring
*/
export function renderToString(element) {
const renderer = new ReactPartialRenderer(element, false);
export function renderToString(element, trustedTypes) {
const renderer = new ReactPartialRenderer(element, false, trustedTypes);
try {
const markup = renderer.read(Infinity);
return markup;
Expand All @@ -27,8 +27,8 @@ export function renderToString(element) {
* such as data-react-id that React uses internally.
* See https://reactjs.org/docs/react-dom-server.html#rendertostaticmarkup
*/
export function renderToStaticMarkup(element) {
const renderer = new ReactPartialRenderer(element, true);
export function renderToStaticMarkup(element, trustedTypes) {
const renderer = new ReactPartialRenderer(element, true, trustedTypes);
try {
const markup = renderer.read(Infinity);
return markup;
Expand Down
28 changes: 24 additions & 4 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import type {ThreadID} from './ReactThreadIDAllocator';
import type {ReactElement} from 'shared/ReactElementType';
import type {ReactProvider, ReactContext} from 'shared/ReactTypes';
import type {TrustedTypes} from './trustedTypes';

import React from 'react';
import invariant from 'shared/invariant';
Expand Down Expand Up @@ -273,10 +274,16 @@ function shouldConstruct(Component) {
return Component.prototype && Component.prototype.isReactComponent;
}

function getNonChildrenInnerMarkup(props) {
function getNonChildrenInnerMarkup(props, trustedTypes) {
const innerHTML = props.dangerouslySetInnerHTML;
if (innerHTML != null) {
if (innerHTML.__html != null) {
if (trustedTypes && trustedTypes.isHTML(innerHTML.__html) === false) {
throw new Error(
'dangerouslySetInnerHTML requires TrustedHTML! Received: ' +
innerHTML.__html,
);
}
return innerHTML.__html;
}
} else {
Expand Down Expand Up @@ -349,6 +356,7 @@ function createOpenTagMarkup(
namespace: string,
makeStaticMarkup: boolean,
isRootElement: boolean,
trustedTypes: ?TrustedTypes,
): string {
let ret = '<' + tagVerbatim;

Expand All @@ -369,7 +377,12 @@ function createOpenTagMarkup(
markup = createMarkupForCustomAttribute(propKey, propValue);
}
} else {
markup = createMarkupForProperty(propKey, propValue);
markup = createMarkupForProperty(
propKey,
propValue,
tagLowercase,
trustedTypes,
);
}
if (markup) {
ret += ' ' + markup;
Expand Down Expand Up @@ -693,14 +706,19 @@ class ReactDOMServerRenderer {
currentSelectValue: any;
previousWasTextNode: boolean;
makeStaticMarkup: boolean;
trustedTypes: ?TrustedTypes;
suspenseDepth: number;

contextIndex: number;
contextStack: Array<ReactContext<any>>;
contextValueStack: Array<any>;
contextProviderStack: ?Array<ReactProvider<any>>; // DEV-only

constructor(children: mixed, makeStaticMarkup: boolean) {
constructor(
children: mixed,
makeStaticMarkup: boolean,
trustedTypes?: TrustedTypes,
) {
const flatChildren = flattenTopLevelChildren(children);

const topFrame: Frame = {
Expand All @@ -722,6 +740,7 @@ class ReactDOMServerRenderer {
this.currentSelectValue = null;
this.previousWasTextNode = false;
this.makeStaticMarkup = makeStaticMarkup;
this.trustedTypes = trustedTypes;
this.suspenseDepth = 0;

// Context (new API)
Expand Down Expand Up @@ -1467,6 +1486,7 @@ class ReactDOMServerRenderer {
namespace,
this.makeStaticMarkup,
this.stack.length === 1,
this.trustedTypes,
);
let footer = '';
if (omittedCloseTags.hasOwnProperty(tag)) {
Expand All @@ -1476,7 +1496,7 @@ class ReactDOMServerRenderer {
footer = '</' + element.type + '>';
}
let children;
const innerMarkup = getNonChildrenInnerMarkup(props);
const innerMarkup = getNonChildrenInnerMarkup(props, this.trustedTypes);
if (innerMarkup != null) {
children = [];
if (newlineEatingTags[tag] && innerMarkup.charAt(0) === '\n') {
Expand Down
169 changes: 169 additions & 0 deletions packages/react-dom/src/server/__tests__/trustedTypes-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
describe('when Trusted Types are passed as parameter to ReactDOM', () => {
let React;
let ReactDOMServer;
let TrustedTypes;
let getPropertyTypeSpy;

beforeEach(() => {
React = require('react');
ReactDOMServer = require('react-dom/server');
TrustedTypes = {
isHTML: value => value.toString() === 'TRUSTED',
isScript: () => true,
isScriptURL: () => true,
isURL: () => true,
getPropertyType: (elem, prop) => {
if (elem === 'div' && prop === 'innerHTML') {
return 'TrustedHTML';
} else {
return undefined;
}
},
};
getPropertyTypeSpy = jest.spyOn(TrustedTypes, 'getPropertyType');
});

describe('rendering safe properties', () => {
it('renders using renderToString method', () => {
const html = ReactDOMServer.renderToString(
<div foo="foo" />,
TrustedTypes,
);
expect(html).toBe('<div foo="foo" data-reactroot=""></div>');
expect(getPropertyTypeSpy).toHaveBeenCalledWith('div', 'foo');
});

it('renders using renderToStaticMarkup method', () => {
const html = ReactDOMServer.renderToStaticMarkup(
<div foo="foo" />,
TrustedTypes,
);
expect(html).toBe('<div foo="foo"></div>');
expect(getPropertyTypeSpy).toHaveBeenCalledWith('div', 'foo');
});

it('renders using renderToNodeStream method', () => {
const stream = ReactDOMServer.renderToNodeStream(
<div foo="foo" />,
TrustedTypes,
).setEncoding('utf8');
expect(stream.read()).toBe('<div foo="foo" data-reactroot=""></div>');
expect(getPropertyTypeSpy).toHaveBeenCalledWith('div', 'foo');
});

it('renders using renderToStaticNodeStream method', () => {
const stream = ReactDOMServer.renderToStaticNodeStream(
<div foo="foo" />,
TrustedTypes,
).setEncoding('utf8');
expect(stream.read()).toBe('<div foo="foo"></div>');
expect(getPropertyTypeSpy).toHaveBeenCalledWith('div', 'foo');
});
});

describe('assigning trusted values into execution sinks', () => {
let trustedValue;
let isHTMLSpy;

beforeEach(() => {
trustedValue = {toString: () => 'TRUSTED'};
isHTMLSpy = jest.spyOn(TrustedTypes, 'isHTML');
});

it('renders using renderToString method', () => {
const html = ReactDOMServer.renderToString(
<div dangerouslySetInnerHTML={{__html: trustedValue}} />,
TrustedTypes,
);
expect(html).toBe('<div data-reactroot="">TRUSTED</div>');
expect(isHTMLSpy).toHaveBeenCalledWith(trustedValue);
});

it('renders using renderToStaticMarkup method', () => {
const html = ReactDOMServer.renderToStaticMarkup(
<div dangerouslySetInnerHTML={{__html: trustedValue}} />,
TrustedTypes,
);
expect(html).toBe('<div>TRUSTED</div>');
expect(isHTMLSpy).toHaveBeenCalledWith(trustedValue);
});

it('renders using renderToNodeStream method', () => {
const stream = ReactDOMServer.renderToNodeStream(
<div dangerouslySetInnerHTML={{__html: trustedValue}} />,
TrustedTypes,
).setEncoding('utf8');
expect(stream.read()).toBe('<div data-reactroot="">TRUSTED</div>');
expect(isHTMLSpy).toHaveBeenCalledWith(trustedValue);
});

it('renders using renderToStaticNodeStream method', () => {
const stream = ReactDOMServer.renderToStaticNodeStream(
<div dangerouslySetInnerHTML={{__html: trustedValue}} />,
TrustedTypes,
).setEncoding('utf8');
expect(stream.read()).toBe('<div>TRUSTED</div>');
expect(isHTMLSpy).toHaveBeenCalledWith(trustedValue);
});
});

describe('when untrusted values are assigned to execution sinks', () => {
let untrustedValue;
let isHTMLSpy;

beforeEach(() => {
untrustedValue = {toString: () => 'untrusted'};
isHTMLSpy = jest.spyOn(TrustedTypes, 'isHTML');
});

it('throws when using renderToString method', () => {
expect(() => {
ReactDOMServer.renderToString(
<div dangerouslySetInnerHTML={{__html: untrustedValue}} />,
TrustedTypes,
);
}).toThrow();
expect(isHTMLSpy).toHaveBeenCalledWith(untrustedValue);
});

it('throws when using renderToStaticMarkup method', () => {
expect(() => {
ReactDOMServer.renderToStaticMarkup(
<div dangerouslySetInnerHTML={{__html: untrustedValue}} />,
TrustedTypes,
);
}).toThrow();
expect(isHTMLSpy).toHaveBeenCalledWith(untrustedValue);
});

it('throws when using renderToNodeStream method', () => {
const response = ReactDOMServer.renderToNodeStream(
<div dangerouslySetInnerHTML={{__html: untrustedValue}} />,
TrustedTypes,
).setEncoding('utf8');

return new Promise(resolve => {
response.once('error', () => {
resolve();
});
expect(response.read()).toBeNull();
expect(isHTMLSpy).toHaveBeenCalledWith(untrustedValue);
});
});

it('throws when using renderToStaticNodeStream method', () => {
const response = ReactDOMServer.renderToStaticNodeStream(
<div dangerouslySetInnerHTML={{__html: untrustedValue}} />,
TrustedTypes,
).setEncoding('utf8');

return new Promise(resolve => {
response.once('error', () => {
resolve();
});
expect(response.read()).toBeNull();
expect(isHTMLSpy).toHaveBeenCalledWith(untrustedValue);
});
});
});
});
Loading