From fed1dac48fa7d7416f2f3d997a4fca214cea901e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4usler?= Date: Fri, 18 Feb 2022 11:01:25 +0100 Subject: [PATCH] Fix XSS in HTML table paste content (#3002) --- src/muya/lib/contentState/copyCutCtrl.js | 4 +- src/muya/lib/contentState/pasteCtrl.js | 37 ++++++++++--------- .../render/renderBlock/renderLeafBlock.js | 7 ++-- .../lib/parser/render/renderInlines/image.js | 1 + src/muya/lib/prism/index.js | 4 +- src/muya/lib/prism/loadLanguage.js | 2 +- src/muya/lib/selection/dom.js | 21 +++++------ 7 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/muya/lib/contentState/copyCutCtrl.js b/src/muya/lib/contentState/copyCutCtrl.js index 998c04fc..7e37429d 100644 --- a/src/muya/lib/contentState/copyCutCtrl.js +++ b/src/muya/lib/contentState/copyCutCtrl.js @@ -45,7 +45,7 @@ const copyCutCtrl = ContentState => { this.partialRender() } - ContentState.prototype.getClipBoradData = function () { + ContentState.prototype.getClipBoardData = function () { const { start, end } = selection.getCursorRange() if (!start || !end) { return { html: '', text: '' } @@ -274,7 +274,7 @@ const copyCutCtrl = ContentState => { return } - const { html, text } = this.getClipBoradData() + const { html, text } = this.getClipBoardData() switch (type) { case 'normal': { event.clipboardData.setData('text/html', html) diff --git a/src/muya/lib/contentState/pasteCtrl.js b/src/muya/lib/contentState/pasteCtrl.js index ee33b375..389aeae5 100644 --- a/src/muya/lib/contentState/pasteCtrl.js +++ b/src/muya/lib/contentState/pasteCtrl.js @@ -41,17 +41,17 @@ const pasteCtrl = ContentState => { } // Try to identify the data type. - ContentState.prototype.checkCopyType = function (html, text) { + ContentState.prototype.checkCopyType = function (html, rawText) { let type = 'normal' - if (!html && text) { + if (!html && rawText) { type = 'copyAsMarkdown' - const match = /^<([a-zA-Z\d-]+)(?=\s|>).*?>[\s\S]+?<\/([a-zA-Z\d-]+)>$/.exec(text.trim()) + const match = /^<([a-zA-Z\d-]+)(?=\s|>).*?>[\s\S]+?<\/([a-zA-Z\d-]+)>$/.exec(rawText.trim()) if (match && match[1]) { const tag = match[1] if (tag === 'table' && match.length === 3 && match[2] === 'table') { // Try to import a single table const tmp = document.createElement('table') - tmp.innerHTML = text + tmp.innerHTML = sanitize(rawText, PREVIEW_DOMPURIFY_CONFIG, false) if (tmp.childElementCount === 1) { return 'htmlToMd' } @@ -64,17 +64,17 @@ const pasteCtrl = ContentState => { return type } - ContentState.prototype.standardizeHTML = async function (html) { + ContentState.prototype.standardizeHTML = async function (rawHtml) { // Only extract the `body.innerHTML` when the `html` is a full HTML Document. - if (/[\s\S]*<\/body>/.test(html)) { - const match = /([\s\S]*)<\/body>/.exec(html) + if (/[\s\S]*<\/body>/.test(rawHtml)) { + const match = /([\s\S]*)<\/body>/.exec(rawHtml) if (match && typeof match[1] === 'string') { - html = match[1] + rawHtml = match[1] } } // Prevent XSS and sanitize HTML. - const sanitizedHtml = sanitize(html, PREVIEW_DOMPURIFY_CONFIG, false) + const sanitizedHtml = sanitize(rawHtml, PREVIEW_DOMPURIFY_CONFIG, false) const tempWrapper = document.createElement('div') tempWrapper.innerHTML = sanitizedHtml @@ -98,9 +98,9 @@ const pasteCtrl = ContentState => { const tds = table.querySelectorAll('td') for (const td of tds) { - const rawHtml = td.innerHTML - if (/
/.test(rawHtml)) { - td.innerHTML = rawHtml.replace(/
/g, '<br>') + const tableDataHtml = td.innerHTML + if (/
/.test(tableDataHtml)) { + td.innerHTML = tableDataHtml.replace(/
/g, '<br>') } } } @@ -110,11 +110,10 @@ const pasteCtrl = ContentState => { for (const link of links) { const href = link.getAttribute('href') const text = link.textContent - - if (href === text) { + if (URL_REG.test(href) && href === text) { const title = await getPageTitle(href) if (title) { - link.textContent = title + link.innerHTML = sanitize(title, PREVIEW_DOMPURIFY_CONFIG, true) } else { const span = document.createElement('span') span.innerHTML = text @@ -266,13 +265,17 @@ const pasteCtrl = ContentState => { const text = rawText || event.clipboardData.getData('text/plain') let html = rawHtml || event.clipboardData.getData('text/html') + if (!text && !html) { + return + } // Support pasted URLs from Firefox. if (URL_REG.test(text) && !/\s/.test(text) && !html) { html = `${text}` } - // Remove crap from HTML such as meta data and styles. + // Remove crap from HTML such as meta data and styles and sanitize HTML, + // but `text` may still contain dangerous HTML. html = await this.standardizeHTML(html) let copyType = this.checkCopyType(html, text) @@ -282,7 +285,7 @@ const pasteCtrl = ContentState => { const parent = this.getParent(startBlock) if (copyType === 'htmlToMd') { - html = text + html = sanitize(text, PREVIEW_DOMPURIFY_CONFIG, false) copyType = 'normal' } diff --git a/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js b/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js index 3f994cbe..8405891a 100644 --- a/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js +++ b/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js @@ -1,5 +1,5 @@ import katex from 'katex' -import prism, { loadedLanguages, transfromAliasToOrigin } from '../../../prism/' +import prism, { loadedLanguages, transformAliasToOrigin } from '../../../prism/' import 'katex/dist/contrib/mhchem.min.js' import { CLASS_OR_ID, DEVICE_MEMORY, PREVIEW_DOMPURIFY_CONFIG, HAS_TEXT_BLOCK_REG } from '../../../config' import { tokenizer } from '../../' @@ -114,7 +114,6 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u this.tokenCache.set(text, tokens) } } - children = tokens.reduce((acc, token) => [...acc, ...this[snakeToCamel(token.type)](h, cursor, block, token)], []) } @@ -233,8 +232,8 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u .replace(new RegExp(MARKER_HASK['"'], 'g'), '"') .replace(new RegExp(MARKER_HASK["'"], 'g'), "'") - // transfrom alias to original language - const transformedLang = transfromAliasToOrigin([lang])[0] + // transform alias to original language + const transformedLang = transformAliasToOrigin([lang])[0] if (transformedLang && /\S/.test(code) && loadedLanguages.has(transformedLang)) { const wrapper = document.createElement('div') wrapper.classList.add(`language-${transformedLang}`) diff --git a/src/muya/lib/parser/render/renderInlines/image.js b/src/muya/lib/parser/render/renderInlines/image.js index 84d0a7ae..194ef355 100644 --- a/src/muya/lib/parser/render/renderInlines/image.js +++ b/src/muya/lib/parser/render/renderInlines/image.js @@ -41,6 +41,7 @@ export default function image (h, cursor, block, token, outerClass) { if (src) { ({ id, isSuccess, domsrc } = this.loadImageAsync(imageInfo, token.attrs)) } + let wrapperSelector = id ? `span#${isSuccess ? block.key + '_' + id + '_' + token.range.start : id}.${CLASS_OR_ID.AG_INLINE_IMAGE}` : `span.${CLASS_OR_ID.AG_INLINE_IMAGE}` diff --git a/src/muya/lib/prism/index.js b/src/muya/lib/prism/index.js index 52574e02..d9e71bb0 100644 --- a/src/muya/lib/prism/index.js +++ b/src/muya/lib/prism/index.js @@ -1,6 +1,6 @@ import Prism from 'prismjs' import { filter } from 'fuzzaldrin' -import initLoadLanguage, { loadedLanguages, transfromAliasToOrigin } from './loadLanguage' +import initLoadLanguage, { loadedLanguages, transformAliasToOrigin } from './loadLanguage' import { languages } from 'prismjs/components.js' const prism = Prism @@ -45,7 +45,7 @@ export { search, loadLanguage, loadedLanguages, - transfromAliasToOrigin + transformAliasToOrigin } export default prism diff --git a/src/muya/lib/prism/loadLanguage.js b/src/muya/lib/prism/loadLanguage.js index 3843aec0..841fd13a 100644 --- a/src/muya/lib/prism/loadLanguage.js +++ b/src/muya/lib/prism/loadLanguage.js @@ -11,7 +11,7 @@ export const loadedLanguages = new Set(['markup', 'css', 'clike', 'javascript']) const { languages } = components // Look for the origin languge by alias -export const transfromAliasToOrigin = langs => { +export const transformAliasToOrigin = langs => { const result = [] for (const lang of langs) { if (languages[lang]) { diff --git a/src/muya/lib/selection/dom.js b/src/muya/lib/selection/dom.js index d0930829..26d3971d 100644 --- a/src/muya/lib/selection/dom.js +++ b/src/muya/lib/selection/dom.js @@ -14,10 +14,9 @@ export const getTextContent = (node, blackList) => { if (blackList.some(className => node.classList && node.classList.contains(className))) { return text } - if (node.nodeType === 3) { - text += node.textContent - } else if (node.nodeType === 1 && node.classList.contains('ag-inline-image')) { - // handle inline image + + // Handle inline image + if (node.nodeType === 1 && node.classList.contains('ag-inline-image')) { const raw = node.getAttribute('data-raw') const imageContainer = node.querySelector('.ag-image-container') const hasImg = imageContainer.querySelector('img') @@ -30,14 +29,14 @@ export const getTextContent = (node, blackList) => { text += child.textContent } } - } else { - text += raw - } - } else { - const childNodes = node.childNodes - for (const n of childNodes) { - text += getTextContent(n, blackList) + return text } + return text + raw + } + + const childNodes = node.childNodes + for (const n of childNodes) { + text += getTextContent(n, blackList) } return text }