diff --git a/package.json b/package.json
index 77b8e1b38..4642977bd 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"brace": "^0.11.1",
"core-js": "^2.5.1",
"custom-event-polyfill": "^0.3.0",
+ "dompurify": "^1.0.8",
"draft-convert": "^2.0.0",
"draft-js": "^0.10.3",
"draft-js-export-html": "^1.2.0",
diff --git a/src/react/Editor/convertToHTML.js b/src/react/Editor/convertToHTML.js
index e1a5a6467..b783d9542 100644
--- a/src/react/Editor/convertToHTML.js
+++ b/src/react/Editor/convertToHTML.js
@@ -4,6 +4,8 @@
import React from 'react';
import { convertToHTML } from 'draft-convert';
+import { sanitizeHTML } from '../utils/utils';
+
export default contentState =>
convertToHTML({
styleToHTML: () => {},
@@ -22,7 +24,7 @@ export default contentState =>
);
diff --git a/src/react/components/AuthorSelectOption.js b/src/react/components/AuthorSelectOption.js
index 7e302a022..dac71801b 100644
--- a/src/react/components/AuthorSelectOption.js
+++ b/src/react/components/AuthorSelectOption.js
@@ -1,6 +1,8 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
+import { sanitizeHTML } from '../utils/utils';
+
class AuthorSelectOption extends Component {
handleMouseDown(event) {
const { onSelect, option } = this.props;
@@ -28,7 +30,10 @@ class AuthorSelectOption extends Component {
onMouseEnter={this.handleMouseEnter.bind(this)}
onMouseMove={this.handleMouseMove.bind(this)}
>
- { option.avatar && }
+ {
+ option.avatar &&
+
+ }
{option.name}
);
diff --git a/src/react/components/Event.js b/src/react/components/Event.js
index 52bae8725..c47e50515 100644
--- a/src/react/components/Event.js
+++ b/src/react/components/Event.js
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { timeAgo } from '../utils/utils';
+
+import { timeAgo, sanitizeHTML } from '../utils/utils';
const Event = ({ event, click, onDelete, canEdit, utcOffset, dateFormat }) => (
@@ -14,7 +15,7 @@ const Event = ({ event, click, onDelete, canEdit, utcOffset, dateFormat }) => (
diff --git a/src/react/containers/EntryContainer.js b/src/react/containers/EntryContainer.js
index f39efd55a..db8e8b036 100644
--- a/src/react/containers/EntryContainer.js
+++ b/src/react/containers/EntryContainer.js
@@ -5,7 +5,7 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as apiActions from '../actions/apiActions';
import * as userActions from '../actions/userActions';
-import { triggerOembedLoad, timeAgo, formattedTime } from '../utils/utils';
+import { triggerOembedLoad, timeAgo, formattedTime, sanitizeHTML } from '../utils/utils';
import EditorContainer from '../containers/EditorContainer';
class EntryContainer extends Component {
@@ -88,10 +88,10 @@ class EntryContainer extends Component {
{ author.avatar &&
+ dangerouslySetInnerHTML={{ __html: sanitizeHTML(author.avatar) }} />
}
+ dangerouslySetInnerHTML={{ __html: sanitizeHTML(author.name) }} />
))
}
@@ -107,7 +107,7 @@ class EntryContainer extends Component {
: (
)
}
diff --git a/src/react/containers/PreviewContainer.js b/src/react/containers/PreviewContainer.js
index d9ec77ace..92776bb9a 100644
--- a/src/react/containers/PreviewContainer.js
+++ b/src/react/containers/PreviewContainer.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { getPreview } from '../services/api';
import Loader from '../components/Loader';
+import { sanitizeHTML } from '../utils/utils';
class PreviewContainer extends Component {
constructor(props) {
@@ -41,7 +42,7 @@ class PreviewContainer extends Component {
return (
);
}
diff --git a/src/react/utils/utils.js b/src/react/utils/utils.js
index 9440d7faf..e95c3ee04 100644
--- a/src/react/utils/utils.js
+++ b/src/react/utils/utils.js
@@ -1,3 +1,5 @@
+import DOMPurify from 'dompurify';
+
import moment from './extendedMoment';
/* eslint-disable no-param-reassign */
@@ -200,3 +202,35 @@ export const getScrollToId = (entries, key) => {
return `id_${entries[0].id}`;
};
+
+/**
+ * Sanitize HTML for output. Help prevent XSS attacks.
+ *
+ * @link https://www.npmjs.com/package/dompurify
+ * @param {string} dirty HTML to sanitize
+ * @return {string} sanitized HTML
+ */
+export const sanitizeHTML = (dirty) => {
+ // Whitelist iframes for the plugins 'embded media' feature.
+ const iframeWhitelist = [
+ 'www.hulu.com',
+ 'player.hulu.com',
+ 'open.spotify.com',
+ 'player.vimeo.com',
+ 'www.youtube.com',
+ // Instagram and Twitter don't use iframes.
+ ];
+ const regex = RegExp(`^(https:|)//(${iframeWhitelist.join('|')})/`, 'im');
+ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
+ if (node.tagName === 'IFRAME') {
+ const iframeSrc = node.getAttribute('src');
+ if (iframeSrc && !iframeSrc.match(regex)) {
+ node.removeAttribute('src');
+ }
+ }
+ });
+ const clean = DOMPurify.sanitize(dirty, {
+ ADD_TAGS: ['iframe'],
+ });
+ return clean;
+};