The sanitising functionality for both html and SVG has been unified into a single file on both the client and server side. Second, a problem with the SVG output from the drawio has also been fixed. Last, it is possible to add a customised path for the drawio web application, which can be used for local hosting the application.

pull/7843/head
Jørn Gustav Larsen 3 weeks ago
parent 54d21ae538
commit abab623240

2
.gitignore vendored

@ -15,6 +15,8 @@ npm/node_modules
.node_repl_history .node_repl_history
npm-debug.log* npm-debug.log*
.yarn .yarn
package-lock.json
yarn.lock
# Generated assets # Generated assets
/assets /assets

@ -0,0 +1,48 @@
import DOMPurify from 'dompurify'
/* global siteConfig */
export default function drawioSanitize(
content) {
if (siteConfig.drawio.sanitizing === false) {
return content
}
console.log('DEBUG: Performing sanitizing ...')
content =
DOMPurify.sanitize(
content,
{
USE_PROFILES:
{
svg: true,
html: true
},
HTML_INTEGRATION_POINTS:
{
foreignobject: true
},
ADD_TAGS: [
'div',
'foreignObject',
'switch',
'style',
'title',
'desc',
'metadata'],
ADD_ATTR: [
'xmlns', 'xmlns:xlink', 'xlink:href', 'xml:space', 'xml:base',
'font-family', 'font-size', 'font-style', 'font-weight',
'alignment-baseline', 'dominant-baseline', 'baseline-shift',
'vector-effect', 'text-anchor', 'clip-path', 'mask',
'fill-rule', 'stroke-linejoin', 'stroke-linecap',
'transform', 'viewBox', 'preserveAspectRatio',
'overflow', 'filter', 'style', 'data-name', 'aria-label', 'requiredFeatures', 'pointer-events'],
FORBID_TAGS: ['script'], // keep scripting disabled
FORBID_ATTR: ['onload', 'onclick', 'onmouseover'], // prevent XSS
ALLOW_UNKNOWN_PROTOCOLS: true // for xlink:href
})
return content
}

@ -152,6 +152,7 @@ import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js' import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css' import 'codemirror/addon/fold/foldgutter.css'
import cmFold from './common/cmFold' import cmFold from './common/cmFold'
import drawioSanitize from './common/sanitize'
// ======================================== // ========================================
// INIT // INIT
@ -227,9 +228,7 @@ export default {
$(elm).parent().replaceWith(`<pre class="diagram">${diagramContent}</div>`) $(elm).parent().replaceWith(`<pre class="diagram">${diagramContent}</div>`)
}) })
this.previewHTML = DOMPurify.sanitize($.html(), { this.previewHTML = drawioSanitize($.html())
ADD_TAGS: ['foreignObject']
})
}, },
/** /**
* Insert content at cursor * Insert content at cursor

@ -226,6 +226,7 @@ import mermaid from 'mermaid'
import katexHelper from './common/katex' import katexHelper from './common/katex'
import tabsetHelper from './markdown/tabset' import tabsetHelper from './markdown/tabset'
import cmFold from './common/cmFold' import cmFold from './common/cmFold'
import drawioSanitize from './common/sanitize'
// ======================================== // ========================================
// INIT // INIT
@ -453,9 +454,7 @@ export default {
linesMap = [] linesMap = []
// this.$store.set('editor/content', newContent) // this.$store.set('editor/content', newContent)
this.processMarkers(this.cm.firstLine(), this.cm.lastLine()) this.processMarkers(this.cm.firstLine(), this.cm.lastLine())
this.previewHTML = DOMPurify.sanitize(md.render(newContent), { this.previewHTML = drawioSanitize(md.render(newContent))
ADD_TAGS: ['foreignObject']
})
this.$nextTick(() => { this.$nextTick(() => {
tabsetHelper.format() tabsetHelper.format()
this.renderMermaidDiagrams() this.renderMermaidDiagrams()

@ -2,7 +2,7 @@
v-card.editor-modal-drawio.animated.fadeIn(flat, tile) v-card.editor-modal-drawio.animated.fadeIn(flat, tile)
iframe( iframe(
ref='drawio' ref='drawio'
src='https://embed.diagrams.net/?embed=1&proto=json&spin=1&saveAndExit=1&noSaveBtn=1&noExitBtn=0' :src='drawioBaseUrl'
frameborder='0' frameborder='0'
) )
</template> </template>
@ -10,22 +10,6 @@
<script> <script>
import { sync, get } from 'vuex-pathify' import { sync, get } from 'vuex-pathify'
// const xmlTest = `<?xml version="1.0" encoding="UTF-8"?>
// <mxfile version="13.4.2">
// <diagram id="SgbkCjxR32CZT1FvBvkp" name="Page-1">
// <mxGraphModel dx="2062" dy="1123" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
// <root>
// <mxCell id="0" />
// <mxCell id="1" parent="0" />
// <mxCell id="5gE3BTvRYS_8FoJnOusC-1" value="" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
// <mxGeometry x="380" y="530" width="80" height="80" as="geometry" />
// </mxCell>
// </root>
// </mxGraphModel>
// </diagram>
// </mxfile>
// `
export default { export default {
data() { data() {
return { return {
@ -34,7 +18,10 @@ export default {
}, },
computed: { computed: {
editorKey: get('editor/editorKey'), editorKey: get('editor/editorKey'),
activeModal: sync('editor/activeModal') activeModal: sync('editor/activeModal'),
drawioBaseUrl() {
return `${siteConfig.drawio.baseUrl}?embed=1&proto=json&spin=1&saveAndExit=1&noSaveBtn=1&noExitBtn=0`
}
}, },
methods: { methods: {
close () { close () {
@ -66,6 +53,15 @@ export default {
this.$store.set('editor/activeModalData', null) this.$store.set('editor/activeModalData', null)
break break
} }
case 'configure': {
this.send({
action: 'configure',
config: {
showStartScreen: true
}
})
break
}
case 'save': { case 'save': {
if (msg.exit) { if (msg.exit) {
this.send({ this.send({
@ -80,7 +76,6 @@ export default {
this.$root.$emit('editorInsert', { this.$root.$emit('editorInsert', {
kind: 'DIAGRAM', kind: 'DIAGRAM',
text: msg.data.slice(svgDataStart) text: msg.data.slice(svgDataStart)
// text: msg.xml.replace(/ agent="(.*?)"/, '').replace(/ host="(.*?)"/, '').replace(/ etag="(.*?)"/, '')
}) })
this.close() this.close()
break break

@ -1,6 +1,6 @@
{ {
"name": "wiki", "name": "wiki",
"version": "2.0.0", "version": "2.5.308",
"releaseDate": "2019-01-01T01:01:01.000Z", "releaseDate": "2019-01-01T01:01:01.000Z",
"description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown", "description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown",
"main": "wiki.js", "main": "wiki.js",
@ -71,7 +71,7 @@
"dependency-graph": "0.11.0", "dependency-graph": "0.11.0",
"diff": "4.0.2", "diff": "4.0.2",
"diff2html": "3.1.14", "diff2html": "3.1.14",
"dompurify": "3.2.6", "dompurify": "3.3.0",
"dotize": "0.3.0", "dotize": "0.3.0",
"elasticsearch6": "npm:@elastic/elasticsearch@6", "elasticsearch6": "npm:@elastic/elasticsearch@6",
"elasticsearch7": "npm:@elastic/elasticsearch@7", "elasticsearch7": "npm:@elastic/elasticsearch@7",

@ -113,6 +113,9 @@ defaults:
search: search:
maxHits: 100 maxHits: 100
maintainerEmail: security@requarks.io maintainerEmail: security@requarks.io
drawio:
baseUrl: https://embed.diagrams.net/
sanitizing: true
localeNamespaces: localeNamespaces:
- admin - admin
- auth - auth

@ -114,6 +114,9 @@ router.get(['/e', '/e/*'], async (req, res, next) => {
_.set(res, 'locals.siteConfig.lang', pageArgs.locale) _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
_.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl') _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
// -> Set Drawio options for editor-modal-drawio.vue
_.set(res, 'locals.siteConfig.drawio', WIKI.config.drawio)
// -> Check for reserved path // -> Check for reserved path
if (pageHelper.isReservedPath(pageArgs.path)) { if (pageHelper.isReservedPath(pageArgs.path)) {
return next(new Error('Cannot create this page because it starts with a system reserved path.')) return next(new Error('Cannot create this page because it starts with a system reserved path.'))

@ -0,0 +1,80 @@
import DOMPurify from 'dompurify'
import { JSDOM } from 'jsdom'
const window = new JSDOM('').window
const purify = DOMPurify(window)
/* global WIKI */
export function sanitizer(
content,
config) {
let purifyConfig = {}
if (('htmlOnly' in config) &&
(config.htmlOnly === true)) {
WIKI.logger.info('Using htmlOnly sanitizer configuration ...')
purifyConfig =
{
USE_PROFILES: {
html: true
},
FORBID_TAGS: ['script'], // keep scripting disabled
FORBID_ATTR: ['onload', 'onclick', 'onmouseover'] // prevent XSS
}
} else if (('svgOnly' in config) &&
(config.svgOnly === true)) {
WIKI.logger.info('Using svgOnly sanitizer configuration')
purifyConfig =
{
USE_PROFILES: {
svg: true,
svgFilters: true
}
}
} else {
WIKI.logger.info('Using renderer sanitizer configuration')
purifyConfig =
{
USE_PROFILES: {
svg: true,
svgFilters: true,
html: true
},
HTML_INTEGRATION_POINTS:
{
foreignobject: true
},
ADD_TAGS: [
'div',
'foreignObject',
'switch',
'style',
'title',
'desc',
'metadata'],
ADD_ATTR: [
'xmlns', 'xmlns:xlink', 'xlink:href', 'xml:space', 'xml:base',
'font-family', 'font-size', 'font-style', 'font-weight',
'alignment-baseline', 'dominant-baseline', 'baseline-shift',
'vector-effect', 'text-anchor', 'clip-path', 'mask',
'fill-rule', 'stroke-linejoin', 'stroke-linecap',
'transform', 'viewBox', 'preserveAspectRatio',
'overflow', 'filter', 'style', 'data-name', 'aria-label', 'requiredFeatures', 'pointer-events'],
FORBID_TAGS: ['script'], // keep scripting disabled
FORBID_ATTR: ['onload', 'onclick', 'onmouseover'], // prevent XSS
ALLOW_UNKNOWN_PROTOCOLS: true // for xlink:href
}
if (!config.allowIFrames) {
purifyConfig.FORBID_TAGS.push('iframe')
purifyConfig.FORBID_ATTR.push('allow')
}
}
return purify.sanitize(
content,
purifyConfig)
}

@ -1,20 +1,13 @@
const fs = require('fs-extra') const fs = require('fs-extra')
const { JSDOM } = require('jsdom') const { sanitizer } = require('../helpers/sanitizer')
const createDOMPurify = require('dompurify')
/* global WIKI */ /* global WIKI */
module.exports = async (svgPath) => { module.exports = async (svgPath) => {
WIKI.logger.info(`Sanitizing SVG file upload...`) WIKI.logger.info(`Sanitizing SVG file upload...`)
try { try {
let svgContents = await fs.readFile(svgPath, 'utf8') let svgContents = await fs.readFile(svgPath, 'utf8')
svgContents = sanitizer(svgContents, {svgOnly: true})
const window = new JSDOM('').window
const DOMPurify = createDOMPurify(window)
svgContents = DOMPurify.sanitize(svgContents)
await fs.writeFile(svgPath, svgContents) await fs.writeFile(svgPath, svgContents)
WIKI.logger.info(`Sanitized SVG file upload: [ COMPLETED ]`) WIKI.logger.info(`Sanitized SVG file upload: [ COMPLETED ]`)
} catch (err) { } catch (err) {

@ -1,16 +1,12 @@
const md = require('markdown-it') const md = require('markdown-it')
const { full: mdEmoji } = require('markdown-it-emoji') const { full: mdEmoji } = require('markdown-it-emoji')
const { JSDOM } = require('jsdom')
const createDOMPurify = require('dompurify')
const _ = require('lodash') const _ = require('lodash')
const { AkismetClient } = require('akismet-api') const { AkismetClient } = require('akismet-api')
const moment = require('moment') const moment = require('moment')
const { sanitizer } = require('../../../helpers/sanitizer')
/* global WIKI */ /* global WIKI */
const window = new JSDOM('').window
const DOMPurify = createDOMPurify(window)
let akismetClient = null let akismetClient = null
const mkdown = md({ const mkdown = md({
@ -65,7 +61,7 @@ module.exports = {
// -> Build New Comment // -> Build New Comment
const newComment = { const newComment = {
content, content,
render: DOMPurify.sanitize(mkdown.render(content)), render: sanitizer(mkdown.render(content), {htmlOnly: true}),
replyTo, replyTo,
pageId: page.id, pageId: page.id,
authorId: user.id, authorId: user.id,
@ -124,7 +120,7 @@ module.exports = {
* Update an existing comment * Update an existing comment
*/ */
async update ({ id, content, user }) { async update ({ id, content, user }) {
const renderedContent = DOMPurify.sanitize(mkdown.render(content)) const renderedContent = sanitizer(mkdown.render(content), {htmlOnly: true})
await WIKI.models.comments.query().findById(id).patch({ await WIKI.models.comments.query().findById(id).patch({
content, content,
render: renderedContent render: renderedContent

@ -1,42 +1,7 @@
const { JSDOM } = require('jsdom') const { sanitizer } = require('../../../helpers/sanitizer')
const createDOMPurify = require('dompurify')
module.exports = { module.exports = {
async init(input, config) { async init(input, config) {
if (config.safeHTML) { return sanitizer(input, config)
const window = new JSDOM('').window
const DOMPurify = createDOMPurify(window)
const allowedAttrs = ['v-pre', 'v-slot:tabs', 'v-slot:content', 'target']
const allowedTags = ['tabset', 'template']
if (config.allowDrawIoUnsafe) {
allowedTags.push('foreignObject')
DOMPurify.addHook('uponSanitizeElement', (elm) => {
if (elm.querySelectorAll) {
const breaks = elm.querySelectorAll('foreignObject br, foreignObject p')
if (breaks && breaks.length) {
for (let i = 0; i < breaks.length; i++) {
breaks[i].parentNode.replaceChild(
window.document.createElement('div'),
breaks[i]
)
}
}
}
})
}
if (config.allowIFrames) {
allowedTags.push('iframe')
allowedAttrs.push('allow')
}
input = DOMPurify.sanitize(input, {
ADD_ATTR: allowedAttrs,
ADD_TAGS: allowedTags
})
}
return input
} }
} }

11342
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save