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; +};