improvement: sanitization via DOMPurify (#4513)

* swap `sanitize-html` for `dompurify`

* set up node enzyme tests with jsdom

dompurify, as the name suggests, needs a DOM or it won't work!

* reconcile tests and sanitizer settings

* remove obsolete sanitizeOptions

* add `jsdom` dependency
This commit is contained in:
kyle
2018-05-04 10:06:44 -07:00
committed by GitHub
parent 8055129dd2
commit 75747424cf
4 changed files with 37 additions and 25 deletions

View File

@@ -31,7 +31,7 @@
"test": "npm run lint-errors && npm run just-test-in-node", "test": "npm run lint-errors && npm run just-test-in-node",
"test-in-node": "npm run lint-errors && npm run just-test-in-node", "test-in-node": "npm run lint-errors && npm run just-test-in-node",
"just-test": "karma start --config karma.conf.js", "just-test": "karma start --config karma.conf.js",
"just-test-in-node": "mocha --recursive --compilers js:babel-core/register test/core test/components test/bugs test/swagger-ui-dist-package test/xss", "just-test-in-node": "mocha --require test/setup.js --recursive --compilers js:babel-core/register test/core test/components test/bugs test/swagger-ui-dist-package test/xss",
"just-check-coverage": "nyc npm run just-test-in-node", "just-check-coverage": "nyc npm run just-test-in-node",
"test-e2e": "sleep 3 && nightwatch test/e2e/scenarios/ --config test/e2e/nightwatch.json", "test-e2e": "sleep 3 && nightwatch test/e2e/scenarios/ --config test/e2e/nightwatch.json",
"e2e-initial-render": "nightwatch test/e2e/scenarios/ --config test/e2e/nightwatch.json --group initial-render", "e2e-initial-render": "nightwatch test/e2e/scenarios/ --config test/e2e/nightwatch.json --group initial-render",
@@ -48,6 +48,7 @@
"core-js": "^2.5.1", "core-js": "^2.5.1",
"css.escape": "1.5.1", "css.escape": "1.5.1",
"deep-extend": "0.4.1", "deep-extend": "0.4.1",
"dompurify": "^1.0.4",
"expect": "1.20.2", "expect": "1.20.2",
"getbase": "^2.8.2", "getbase": "^2.8.2",
"ieee754": "^1.1.8", "ieee754": "^1.1.8",
@@ -80,7 +81,6 @@
"redux-logger": "*", "redux-logger": "*",
"remarkable": "^1.7.1", "remarkable": "^1.7.1",
"reselect": "2.5.3", "reselect": "2.5.3",
"sanitize-html": "^1.14.1",
"scroll-to-element": "^2.0.0", "scroll-to-element": "^2.0.0",
"serialize-error": "2.0.0", "serialize-error": "2.0.0",
"shallowequal": "0.2.2", "shallowequal": "0.2.2",
@@ -119,6 +119,7 @@
"file-loader": "0.11.2", "file-loader": "0.11.2",
"git-describe": "^4.0.1", "git-describe": "^4.0.1",
"imports-loader": "0.7.1", "imports-loader": "0.7.1",
"jsdom": "^11.10.0",
"json-loader": "0.5.4", "json-loader": "0.5.4",
"json-server": "^0.11.0", "json-server": "^0.11.0",
"karma": "^1.7.0", "karma": "^1.7.0",

View File

@@ -1,7 +1,7 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import Remarkable from "remarkable" import Remarkable from "remarkable"
import sanitize from "sanitize-html" import DomPurify from "dompurify"
import cx from "classnames" import cx from "classnames"
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
@@ -40,20 +40,8 @@ Markdown.propTypes = {
export default Markdown export default Markdown
const sanitizeOptions = {
allowedTags: sanitize.defaults.allowedTags.concat([ "h1", "h2", "img", "span" ]),
allowedAttributes: {
...sanitize.defaults.allowedAttributes,
"img": sanitize.defaults.allowedAttributes.img.concat(["title"]),
"td": [ "colspan" ],
"*": [ "class" ]
},
allowedSchemesByTag: { img: [ "http", "https", "data" ] },
textFilter: function(text) {
return text.replace(/"/g, "\"")
}
}
export function sanitizer(str) { export function sanitizer(str) {
return sanitize(str, sanitizeOptions) return DomPurify.sanitize(str, {
ADD_ATTR: ["target"]
})
} }

View File

@@ -16,19 +16,19 @@ describe("Markdown component", function() {
it("allows td elements with colspan attrib", function() { it("allows td elements with colspan attrib", function() {
const str = `<table><tr><td>ABC</td></tr></table>` const str = `<table><tr><td>ABC</td></tr></table>`
const el = render(<Markdown source={str} />) const el = render(<Markdown source={str} />)
expect(el.html()).toEqual(`<div class="markdown"><table><tr><td>ABC</td></tr></table></div>`) expect(el.html()).toEqual(`<div class="markdown"><table><tbody><tr><td>ABC</td></tr></tbody></table></div>`)
}) })
it("allows image elements", function() { it("allows image elements", function() {
const str = `![Image alt text](http://image.source "Image title")` const str = `![Image alt text](http://image.source "Image title")`
const el = render(<Markdown source={str} />) const el = render(<Markdown source={str} />)
expect(el.html()).toEqual(`<div class="markdown"><p><img src="http://image.source" title="Image title"></p>\n</div>`) expect(el.html()).toEqual(`<div class="markdown"><p><img title="Image title" alt="Image alt text" src="http://image.source"></p>\n</div>`)
}) })
it("allows image elements with https scheme", function() { it("allows image elements with https scheme", function() {
const str = `![Image alt text](https://image.source "Image title")` const str = `![Image alt text](https://image.source "Image title")`
const el = render(<Markdown source={str} />) const el = render(<Markdown source={str} />)
expect(el.html()).toEqual(`<div class="markdown"><p><img src="https://image.source" title="Image title"></p>\n</div>`) expect(el.html()).toEqual(`<div class="markdown"><p><img title="Image title" alt="Image alt text" src="https://image.source"></p>\n</div>`)
}) })
it("allows image elements with data scheme", function() { it("allows image elements with data scheme", function() {
@@ -52,7 +52,7 @@ describe("Markdown component", function() {
it("allows links", function() { it("allows links", function() {
const str = `[Link](https://example.com/)` const str = `[Link](https://example.com/)`
const el = render(<Markdown source={str} />) const el = render(<Markdown source={str} />)
expect(el.html()).toEqual(`<div class="markdown"><p><a href="https://example.com/" target="_blank">Link</a></p>\n</div>`) expect(el.html()).toEqual(`<div class="markdown"><p><a target="_blank" href="https://example.com/">Link</a></p>\n</div>`)
}) })
}) })
@@ -60,13 +60,13 @@ describe("Markdown component", function() {
it("allows image elements", function() { it("allows image elements", function() {
const str = `![Image alt text](http://image.source "Image title")` const str = `![Image alt text](http://image.source "Image title")`
const el = render(<OAS3Markdown source={str} />) const el = render(<OAS3Markdown source={str} />)
expect(el.html()).toEqual(`<div class="renderedMarkdown"><div><p><img src="http://image.source" title="Image title"></p></div></div>`) expect(el.html()).toEqual(`<div class="renderedMarkdown"><div><p><img title="Image title" alt="Image alt text" src="http://image.source"></p></div></div>`)
}) })
it("allows image elements with https scheme", function() { it("allows image elements with https scheme", function() {
const str = `![Image alt text](https://image.source "Image title")` const str = `![Image alt text](https://image.source "Image title")`
const el = render(<OAS3Markdown source={str} />) const el = render(<OAS3Markdown source={str} />)
expect(el.html()).toEqual(`<div class="renderedMarkdown"><div><p><img src="https://image.source" title="Image title"></p></div></div>`) expect(el.html()).toEqual(`<div class="renderedMarkdown"><div><p><img title="Image title" alt="Image alt text" src="https://image.source"></p></div></div>`)
}) })
it("allows image elements with data scheme", function() { it("allows image elements with data scheme", function() {

23
test/setup.js Normal file
View File

@@ -0,0 +1,23 @@
const { JSDOM } = require("jsdom")
const win = require("core/window")
const jsdom = new JSDOM("<!doctype html><html><body></body></html>")
const { window } = jsdom
function copyProps(src, target) {
const props = Object.getOwnPropertyNames(src)
.filter(prop => typeof target[prop] === "undefined")
.reduce((result, prop) => ({
...result,
[prop]: Object.getOwnPropertyDescriptor(src, prop),
}), {})
Object.defineProperties(target, props)
}
global.window = window
global.document = window.document
global.navigator = {
userAgent: "node.js",
}
copyProps(win, window) // use UI's built-in window wrapper
copyProps(window, global)