Fix mermaid.js XSS (#2947)

* Fix mermaid.js XSS

* Update vega packages
This commit is contained in:
Felix Häusler 2022-01-29 09:01:39 +01:00 committed by GitHub
parent ecc23a9cb0
commit c959d185b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 557 additions and 325 deletions

View File

@ -67,7 +67,7 @@
"katex": "^0.15.2",
"keyboard-layout": "^2.0.17",
"keytar": "^7.7.0",
"mermaid": "8.8.4",
"mermaid": "^8.13.10",
"minizlib": "^2.1.1",
"plist": "^3.0.4",
"popper.js": "^1.16.1",
@ -78,9 +78,9 @@
"turndown": "^7.1.1",
"underscore": "^1.13.2",
"unsplash-js": "^7.0.15",
"vega": "^5.17.3",
"vega-embed": "^6.14.2",
"vega-lite": "^4.17.0",
"vega": "^5.21.0",
"vega-embed": "^6.20.5",
"vega-lite": "^5.2.0",
"vscode-ripgrep": "^1.12.1",
"vue": "^2.6.14",
"vue-electron": "^1.0.6",

View File

@ -1,6 +1,6 @@
import loadRenderer from '../../renderers'
import { CLASS_OR_ID } from '../../config'
import { conflict, mixins, camelToSnake } from '../../utils'
import { CLASS_OR_ID, PREVIEW_DOMPURIFY_CONFIG } from '../../config'
import { conflict, mixins, camelToSnake, sanitize } from '../../utils'
import { patch, toVNode, toHTML, h, addNStoVNodeSvgChildren } from './snabbdom'
import { beginRules } from '../rules'
import renderInlines from './renderInlines'
@ -99,6 +99,7 @@ class StateRender {
if (this.mermaidCache.size) {
const mermaid = await loadRenderer('mermaid')
mermaid.initialize({
securityLevel: 'strict',
theme: this.muya.options.mermaidTheme
})
for (const [key, value] of this.mermaidCache.entries()) {
@ -109,7 +110,7 @@ class StateRender {
}
try {
mermaid.parse(code)
target.innerHTML = code
target.innerHTML = sanitize(code, PREVIEW_DOMPURIFY_CONFIG, true)
mermaid.init(undefined, target)
} catch (err) {
target.innerHTML = '< Invalid Mermaid Codes >'

View File

@ -38,18 +38,20 @@ class ExportHtml {
for (const code of codes) {
const preEle = code.parentNode
const mermaidContainer = document.createElement('div')
mermaidContainer.innerHTML = code.innerHTML
mermaidContainer.innerHTML = sanitize(unescapeHtml(code.innerHTML), EXPORT_DOMPURIFY_CONFIG, true)
mermaidContainer.classList.add('mermaid')
preEle.replaceWith(mermaidContainer)
}
const mermaid = await loadRenderer('mermaid')
// We only export light theme, so set mermaid theme to `default`, in the future, we can choose whick theme to export.
mermaid.initialize({
securityLevel: 'strict',
theme: 'default'
})
mermaid.init(undefined, this.exportContainer.querySelectorAll('div.mermaid'))
if (this.muya) {
mermaid.initialize({
securityLevel: 'strict',
theme: this.muya.options.mermaidTheme
})
}

View File

@ -220,7 +220,8 @@
<script>
import { mapState } from 'vuex'
import fs from 'fs/promises'
import fs from 'fs'
import fsPromises from 'fs/promises'
import path from 'path'
import { isDirectory, isFile } from 'common/filesystem'
import bus from '../../bus'
@ -420,7 +421,7 @@ export default {
const fullname = path.join(themeDir, filename)
if (/.+\.css$/i.test(filename) && isFile(fullname)) {
try {
const content = await fs.readFile(fullname, 'utf8')
const content = await fsPromises.readFile(fullname, 'utf8')
// Match comment with theme name in first line only.
const match = content.match(/^(?:\/\*+[ \t]*([A-z0-9 -]+)[ \t]*(?:\*+\/|[\n\r])?)/)

View File

@ -1,5 +1,6 @@
import template from './index.html'
import { getUniqueId } from '../../util'
import { sanitize, EXPORT_DOMPURIFY_CONFIG } from '../../util/dompurify'
import './index.css'
const INON_HASH = {
@ -15,6 +16,13 @@ const TYPE_HASH = {
info: 'mt-info'
}
const fillTemplate = (type, title, message) => {
return template
.replace(/\{\{icon\}\}/, INON_HASH[type])
.replace(/\{\{title\}\}/, sanitize(title, EXPORT_DOMPURIFY_CONFIG))
.replace(/\{\{message\}\}/, sanitize(message, EXPORT_DOMPURIFY_CONFIG))
}
const notification = {
name: 'notify',
noticeCache: {},
@ -36,10 +44,7 @@ const notification = {
const id = getUniqueId()
const fragment = document.createElement('div')
fragment.innerHTML = template
.replace(/\{\{icon\}\}/, INON_HASH[type])
.replace(/\{\{title\}\}/, title)
.replace(/\{\{message\}\}/, message)
fragment.innerHTML = fillTemplate(type, title, message)
const noticeContainer = fragment.querySelector('.mt-notification')
const bgNotice = noticeContainer.querySelector('.notice-bg')

View File

@ -0,0 +1,32 @@
import runSanitize from 'muya/lib/utils/dompurify'
export const PREVIEW_DOMPURIFY_CONFIG = Object.freeze({
FORBID_ATTR: ['style', 'contenteditable'],
ALLOW_DATA_ATTR: false,
USE_PROFILES: {
html: true,
svg: true,
svgFilters: true,
mathMl: false
},
RETURN_TRUSTED_TYPE: false
})
export const EXPORT_DOMPURIFY_CONFIG = Object.freeze({
FORBID_ATTR: ['contenteditable'],
ALLOW_DATA_ATTR: false,
ADD_ATTR: ['data-align'],
USE_PROFILES: {
html: true,
svg: true,
svgFilters: true,
mathMl: false
},
RETURN_TRUSTED_TYPE: false,
// Allow "file" protocol to export images on Windows (#1997).
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|file):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
})
export const sanitize = (html, purifyOptions) => {
return runSanitize(html, purifyOptions)
}

View File

@ -1,12 +1,12 @@
import fs from 'fs'
import path from 'path'
import Slugger from 'muya/lib/parser/marked/slugger'
import sanitize from 'muya/lib/utils/dompurify'
import { isFile } from 'common/filesystem'
import { escapeHtml, unescapeHtml } from 'muya/lib/utils'
import academicTheme from '@/assets/themes/export/academic.theme.css'
import liberTheme from '@/assets/themes/export/liber.theme.css'
import { cloneObj } from '../util'
import { sanitize, EXPORT_DOMPURIFY_CONFIG } from '../util/dompurify'
export const getCssForOptions = options => {
const {
@ -130,17 +130,6 @@ export const getHtmlToc = (toc, options = {}) => {
return sanitize(html, EXPORT_DOMPURIFY_CONFIG)
}
const EXPORT_DOMPURIFY_CONFIG = {
FORBID_ATTR: ['contenteditable'],
ALLOW_DATA_ATTR: false,
USE_PROFILES: {
html: true,
svg: true,
svgFilters: true,
mathMl: true
}
}
// Don't use "Noto Color Emoji" because it will result in PDF files with multiple MB and weird looking emojis.
const FALLBACK_FONT_FAMILIES = '"Open Sans","Segoe UI","Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"'

800
yarn.lock

File diff suppressed because it is too large Load Diff