Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ npm/node_modules
.node_repl_history
npm-debug.log*
.yarn
package-lock.json
yarn.lock

# Generated assets
/assets
Expand Down
48 changes: 48 additions & 0 deletions client/components/editor/common/sanitize.js
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 2 additions & 3 deletions client/components/editor/editor-asciidoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css'
import cmFold from './common/cmFold'
import drawioSanitize from './common/sanitize'

// ========================================
// INIT
Expand Down Expand Up @@ -227,9 +228,7 @@ export default {
$(elm).parent().replaceWith(`<pre class="diagram">${diagramContent}</div>`)
})

this.previewHTML = DOMPurify.sanitize($.html(), {
ADD_TAGS: ['foreignObject']
})
this.previewHTML = drawioSanitize($.html())
},
/**
* Insert content at cursor
Expand Down
5 changes: 2 additions & 3 deletions client/components/editor/editor-markdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ import mermaid from 'mermaid'
import katexHelper from './common/katex'
import tabsetHelper from './markdown/tabset'
import cmFold from './common/cmFold'
import drawioSanitize from './common/sanitize'

// ========================================
// INIT
Expand Down Expand Up @@ -453,9 +454,7 @@ export default {
linesMap = []
// this.$store.set('editor/content', newContent)
this.processMarkers(this.cm.firstLine(), this.cm.lastLine())
this.previewHTML = DOMPurify.sanitize(md.render(newContent), {
ADD_TAGS: ['foreignObject']
})
this.previewHTML = drawioSanitize(md.render(newContent))
this.$nextTick(() => {
tabsetHelper.format()
this.renderMermaidDiagrams()
Expand Down
33 changes: 14 additions & 19 deletions client/components/editor/editor-modal-drawio.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,14 @@
v-card.editor-modal-drawio.animated.fadeIn(flat, tile)
iframe(
ref='drawio'
src='https://embed.diagrams.net/?embed=1&proto=json&spin=1&saveAndExit=1&noSaveBtn=1&noExitBtn=0'
:src='drawioBaseUrl'
frameborder='0'
)
</template>

<script>
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 {
data() {
return {
Expand All @@ -34,7 +18,10 @@ export default {
},
computed: {
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: {
close () {
Expand Down Expand Up @@ -66,6 +53,15 @@ export default {
this.$store.set('editor/activeModalData', null)
break
}
case 'configure': {
this.send({
action: 'configure',
config: {
showStartScreen: true
}
})
break
}
case 'save': {
if (msg.exit) {
this.send({
Expand All @@ -80,7 +76,6 @@ export default {
this.$root.$emit('editorInsert', {
kind: 'DIAGRAM',
text: msg.data.slice(svgDataStart)
// text: msg.xml.replace(/ agent="(.*?)"/, '').replace(/ host="(.*?)"/, '').replace(/ etag="(.*?)"/, '')
})
this.close()
break
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wiki",
"version": "2.0.0",
"version": "2.5.308",
"releaseDate": "2019-01-01T01:01:01.000Z",
"description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown",
"main": "wiki.js",
Expand Down Expand Up @@ -71,7 +71,7 @@
"dependency-graph": "0.11.0",
"diff": "4.0.2",
"diff2html": "3.1.14",
"dompurify": "3.2.6",
"dompurify": "3.3.0",
"dotize": "0.3.0",
"elasticsearch6": "npm:@elastic/elasticsearch@6",
"elasticsearch7": "npm:@elastic/elasticsearch@7",
Expand Down
3 changes: 3 additions & 0 deletions server/app/data.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ defaults:
search:
maxHits: 100
maintainerEmail: security@requarks.io
drawio:
baseUrl: https://embed.diagrams.net/
sanitizing: true
localeNamespaces:
- admin
- auth
Expand Down
3 changes: 3 additions & 0 deletions server/controllers/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ router.get(['/e', '/e/*'], async (req, res, next) => {
_.set(res, 'locals.siteConfig.lang', pageArgs.locale)
_.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
if (pageHelper.isReservedPath(pageArgs.path)) {
return next(new Error('Cannot create this page because it starts with a system reserved path.'))
Expand Down
80 changes: 80 additions & 0 deletions server/helpers/sanitizer.js
Original file line number Diff line number Diff line change
@@ -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)
}
11 changes: 2 additions & 9 deletions server/jobs/sanitize-svg.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
const fs = require('fs-extra')
const { JSDOM } = require('jsdom')
const createDOMPurify = require('dompurify')
const { sanitizer } = require('../helpers/sanitizer')

/* global WIKI */

module.exports = async (svgPath) => {
WIKI.logger.info(`Sanitizing SVG file upload...`)

try {
let svgContents = await fs.readFile(svgPath, 'utf8')

const window = new JSDOM('').window
const DOMPurify = createDOMPurify(window)

svgContents = DOMPurify.sanitize(svgContents)

svgContents = sanitizer(svgContents, {svgOnly: true})
await fs.writeFile(svgPath, svgContents)
WIKI.logger.info(`Sanitized SVG file upload: [ COMPLETED ]`)
} catch (err) {
Expand Down
10 changes: 3 additions & 7 deletions server/modules/comments/default/comment.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
const md = require('markdown-it')
const { full: mdEmoji } = require('markdown-it-emoji')
const { JSDOM } = require('jsdom')
const createDOMPurify = require('dompurify')
const _ = require('lodash')
const { AkismetClient } = require('akismet-api')
const moment = require('moment')
const { sanitizer } = require('../../../helpers/sanitizer')

/* global WIKI */

const window = new JSDOM('').window
const DOMPurify = createDOMPurify(window)

let akismetClient = null

const mkdown = md({
Expand Down Expand Up @@ -65,7 +61,7 @@ module.exports = {
// -> Build New Comment
const newComment = {
content,
render: DOMPurify.sanitize(mkdown.render(content)),
render: sanitizer(mkdown.render(content), {htmlOnly: true}),
replyTo,
pageId: page.id,
authorId: user.id,
Expand Down Expand Up @@ -124,7 +120,7 @@ module.exports = {
* Update an existing comment
*/
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({
content,
render: renderedContent
Expand Down
39 changes: 2 additions & 37 deletions server/modules/rendering/html-security/renderer.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,7 @@
const { JSDOM } = require('jsdom')
const createDOMPurify = require('dompurify')
const { sanitizer } = require('../../../helpers/sanitizer')

module.exports = {
async init(input, config) {
if (config.safeHTML) {
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
return sanitizer(input, config)
}
}
Loading