diff --git a/__tests__/src/components/SanitizedHtml.test.js b/__tests__/src/components/SanitizedHtml.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..9d62c9e02d342fe44fa919eb1d13211ff728b0dc
--- /dev/null
+++ b/__tests__/src/components/SanitizedHtml.test.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import SanitizedHtml from '../../../src/components/SanitizedHtml';
+
+const wrapper = shallow(
+  <SanitizedHtml
+    htmlString="<script>doBadThings()</script><b>Don't worry!</b>"
+    ruleSet="iiif"
+  />,
+);
+
+describe('SanitizedHtml', () => {
+  it('should render needed elements', () => {
+    expect(wrapper.find('span').length).toBe(1);
+  });
+
+  it('should pass correct class name to root element', () => {
+    expect(wrapper.find('span').first().props().className).toBe('mirador-third-party-html');
+  });
+
+  it('should pass sanitized html string to dangerouslySetInnerHTML attribute', () => {
+    expect(wrapper.find('span').first().props().dangerouslySetInnerHTML)
+      .toEqual({ __html: "<b>Don't worry!</b>" });
+  });
+});
diff --git a/package.json b/package.json
index da64e5c5f081717f3b973647b80ca8d378187dba..92c84719f18dadc21ac0fd3f38727c2e84afcd00 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
     "classnames": "^2.2.6",
     "css-ns": "^1.2.2",
     "deepmerge": "^3.1.0",
+    "dompurify": "^1.0.9",
     "i18next": "^14.0.1",
     "intersection-observer": "^0.5.1",
     "lodash": "^4.17.11",
diff --git a/src/components/SanitizedHtml.js b/src/components/SanitizedHtml.js
new file mode 100644
index 0000000000000000000000000000000000000000..299bf2eab8f6b93699e9ee2bac4a9b445f39ef5a
--- /dev/null
+++ b/src/components/SanitizedHtml.js
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { sanitize } from 'dompurify';
+import ns from '../config/css-ns';
+import htmlRules from '../lib/htmlRules';
+
+/**
+*/
+class SanitizedHtml extends Component {
+  /**
+  */
+  render() {
+    const { htmlString, ruleSet } = this.props;
+    return (
+      <span
+        className={ns('third-party-html')}
+        dangerouslySetInnerHTML={{ // eslint-disable-line react/no-danger
+          __html: sanitize(htmlString, htmlRules[ruleSet]),
+        }}
+      />
+    );
+  }
+}
+
+SanitizedHtml.propTypes = {
+  ruleSet: PropTypes.string.isRequired,
+  htmlString: PropTypes.string.isRequired,
+};
+
+export default SanitizedHtml;
diff --git a/src/lib/htmlRules.js b/src/lib/htmlRules.js
new file mode 100644
index 0000000000000000000000000000000000000000..30b190428c014a52cc6f44e453803f300212d857
--- /dev/null
+++ b/src/lib/htmlRules.js
@@ -0,0 +1,27 @@
+
+// Only remove security related tags and attributes. Allow each other.
+const liberal = {};
+
+// No html at all. Only text will remain.
+const noHtml = {
+  ALLOWED_TAGS: [],
+};
+
+// Presentation API 2 suggestion.
+const iiif = {
+  ALLOWED_TAGS: ['a', 'b', 'br', 'i', 'img', 'p', 'span'],
+  ALLOWED_ATTR: ['href', 'src', 'alt'],
+};
+
+// Rule set that is used in Mirador 2.
+const mirador2 = {
+  ALLOWED_TAGS: ['a', 'b', 'br', 'i', 'img', 'p', 'span', 'strong', 'em', 'ul', 'ol', 'li'],
+  ALLOWED_ATTR: ['href', 'target', 'src', 'alt', 'dir'],
+};
+
+export default {
+  liberal,
+  noHtml,
+  iiif,
+  mirador2,
+};