mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 22:22:18 +08:00
Fix XSS in HTML table paste content (#3002)
This commit is contained in:
parent
ba8d89149f
commit
fed1dac48f
@ -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)
|
||||
|
@ -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 (/<body>[\s\S]*<\/body>/.test(html)) {
|
||||
const match = /<body>([\s\S]*)<\/body>/.exec(html)
|
||||
if (/<body>[\s\S]*<\/body>/.test(rawHtml)) {
|
||||
const match = /<body>([\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 (/<br>/.test(rawHtml)) {
|
||||
td.innerHTML = rawHtml.replace(/<br>/g, '<br>')
|
||||
const tableDataHtml = td.innerHTML
|
||||
if (/<br>/.test(tableDataHtml)) {
|
||||
td.innerHTML = tableDataHtml.replace(/<br>/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 = `<a href="${text}">${text}</a>`
|
||||
}
|
||||
|
||||
// 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'
|
||||
}
|
||||
|
||||
|
@ -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}`)
|
||||
|
@ -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}`
|
||||
|
@ -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
|
||||
|
@ -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]) {
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user