diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index 05e5e7be..bd3d452e 100644 --- a/browser/lib/markdown-it-sanitize-html.js +++ b/browser/lib/markdown-it-sanitize-html.js @@ -2,6 +2,7 @@ import sanitizeHtml from 'sanitize-html' import { escapeHtmlCharacters } from './utils' +import url from 'url' module.exports = function sanitizePlugin (md, options) { options = options || {} @@ -25,7 +26,7 @@ module.exports = function sanitizePlugin (md, options) { const inlineTokens = state.tokens[tokenIdx].children for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) { if (inlineTokens[childIdx].type === 'html_inline') { - inlineTokens[childIdx].content = sanitizeHtml( + inlineTokens[childIdx].content = sanitizeInline( inlineTokens[childIdx].content, options ) @@ -35,3 +36,89 @@ module.exports = function sanitizePlugin (md, options) { } }) } + +const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i +const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig + +function sanitizeInline (html, options) { + let match = tagRegex.exec(html) + if (!match) { + return '' + } + + const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options + + if (match[1] !== undefined) { + // opening tag + const tag = match[1].toLowerCase() + if (allowedTags.indexOf(tag) === -1) { + return '' + } + + const attributes = match[2] + + let attrs = '' + let name + let value + + while ((match = attributesRegex.exec(attributes))) { + name = match[1].toLowerCase() + value = match[3] + + if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) { + if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) { + if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) { + continue + } + } + + attrs += ` ${name}` + if (match[2]) { + attrs += `="${value}"` + } + } + } + + if (selfClosing.indexOf(tag) === -1) { + return '<' + tag + attrs + '>' + } else { + return '<' + tag + attrs + ' />' + } + } else { + // closing tag + if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) { + return html + } else { + return '' + } + } +} + +function naughtyHRef (href, options) { + // href = href.replace(/[\x00-\x20]+/g, '') + href = href.replace(/<\!\-\-.*?\-\-\>/g, '') + + const matches = href.match(/^([a-zA-Z]+)\:/) + if (!matches) { + if (href.match(/^[\/\\]{2}/)) { + return !options.allowProtocolRelative + } + + // No scheme + return false + } + + const scheme = matches[1].toLowerCase() + + return options.allowedSchemes.indexOf(scheme) === -1 +} + +function naughtyIFrame (src, options) { + try { + const parsed = url.parse(src, false, true) + + return options.allowedIframeHostnames.index(parsed.hostname) === -1 + } catch (e) { + return true + } +} diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 49260740..7fbb5a38 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -106,7 +106,11 @@ class Markdown { 'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'], 'input': ['type', 'id', 'checked'] }, - allowedIframeHostnames: ['www.youtube.com'] + allowedIframeHostnames: ['www.youtube.com'], + selfClosing: [ 'img', 'br', 'hr', 'input' ], + allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ], + allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ], + allowProtocolRelative: true }) } diff --git a/tests/fixtures/markdowns.js b/tests/fixtures/markdowns.js index 69e335e0..312e6c18 100644 --- a/tests/fixtures/markdowns.js +++ b/tests/fixtures/markdowns.js @@ -50,11 +50,14 @@ const smartQuotes = 'This is a "QUOTE".' const breaks = 'This is the first line.\nThis is the second line.' +const shortcuts = 'Ctrl\n\n[[Ctrl]]' + export default { basic, codeblock, katex, checkboxes, smartQuotes, - breaks + breaks, + shortcuts } diff --git a/tests/lib/markdown-test.js b/tests/lib/markdown-test.js index 73b68799..0e330a65 100644 --- a/tests/lib/markdown-test.js +++ b/tests/lib/markdown-test.js @@ -43,3 +43,8 @@ test('Markdown.render() should render line breaks correctly', t => { const renderedNonBreaks = newmd.render(markdownFixtures.breaks) t.snapshot(renderedNonBreaks) }) + +test('Markdown.render() should render shortcuts correctly', t => { + const rendered = md.render(markdownFixtures.shortcuts) + t.snapshot(rendered) +}) diff --git a/tests/lib/snapshots/markdown-test.js.md b/tests/lib/snapshots/markdown-test.js.md index b7251b8d..df5bad53 100644 --- a/tests/lib/snapshots/markdown-test.js.md +++ b/tests/lib/snapshots/markdown-test.js.md @@ -18,6 +18,14 @@ Generated by [AVA](https://ava.li). This is the second line.

␊ ` +## Markdown.render() should render shortcuts correctly + +> Snapshot 1 + + `

Ctrl

␊ +

Ctrl

␊ + ` + ## Markdown.render() should renders KaTeX correctly > Snapshot 1 diff --git a/tests/lib/snapshots/markdown-test.js.snap b/tests/lib/snapshots/markdown-test.js.snap index 9254709e..5b8d1095 100644 Binary files a/tests/lib/snapshots/markdown-test.js.snap and b/tests/lib/snapshots/markdown-test.js.snap differ