fix: prevent user-provided v-slot content

pull/8098/head v2.5.315
NGPixel 1 week ago
parent 2df962c86d
commit 1161ddc6e6
No known key found for this signature in database

@ -1,15 +1,35 @@
const { JSDOM } = require('jsdom')
const createDOMPurify = require('dompurify')
// Vue directives that are allowed in rendered page content. The page HTML is
// compiled by the Vue template compiler on the client, so any value left on a
// directive would be evaluated as a JavaScript expression. These directives are
// only ever used as valueless flags (e.g. by the tabset renderer), so their
// value is always discarded during sanitization.
const allowedDirectives = ['v-pre', 'v-slot:tabs', 'v-slot:content']
// Any attribute the Vue template compiler could pick up as a directive or a
// binding must never carry author-controlled content.
const directiveAttrRegex = /^(v-|:|@|#)/
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 allowedAttrs = [...allowedDirectives, 'target']
const allowedTags = ['tabset', 'template']
DOMPurify.addHook('uponSanitizeAttribute', (elm, data) => {
if (allowedDirectives.includes(data.attrName)) {
// Strip the value so that it can never be compiled as a JS expression
data.attrValue = ''
} else if (directiveAttrRegex.test(data.attrName)) {
data.keepAttr = false
}
})
if (config.allowDrawIoUnsafe) {
allowedTags.push('foreignObject')
DOMPurify.addHook('uponSanitizeElement', (elm) => {

@ -0,0 +1,59 @@
const renderer = require('../../../modules/rendering/html-security/renderer')
describe('modules/rendering/html-security', () => {
const config = {
safeHTML: true,
allowDrawIoUnsafe: false,
allowIFrames: false
}
it('keeps the slot directives emitted by the tabset renderer', async () => {
const input = '<tabset><template v-slot:tabs=""><li>Tab</li></template>' +
'<template v-slot:content=""><div class="tabset-panel"><p>Content</p></div></template></tabset>'
const result = await renderer.init(input, config)
expect(result).toEqual(input)
})
it('strips the value of v-slot:tabs so it cannot be compiled as an expression', async () => {
const input = `<tabset><template v-slot:tabs="{x = constructor.constructor('alert(1)')()}"><li>Tab</li></template></tabset>`
const result = await renderer.init(input, config)
expect(result).toEqual('<tabset><template v-slot:tabs=""><li>Tab</li></template></tabset>')
})
it('strips the value of v-slot:content so it cannot be compiled as an expression', async () => {
const input = `<tabset><template v-slot:content="{x = constructor.constructor('alert(1)')()}"><div>Content</div></template></tabset>`
const result = await renderer.init(input, config)
expect(result).toEqual('<tabset><template v-slot:content=""><div>Content</div></template></tabset>')
})
it('strips the value of v-pre', async () => {
const result = await renderer.init(`<p v-pre="{{ constructor.constructor('alert(1)')() }}">Text</p>`, config)
expect(result).toEqual('<p v-pre="">Text</p>')
})
it('removes any other directive or binding attribute', async () => {
const inputs = [
`<div v-html="'<img src=x onerror=alert(1)>'">Text</div>`,
`<div :class="constructor.constructor('alert(1)')()">Text</div>`,
`<div v-bind:class="constructor.constructor('alert(1)')()">Text</div>`,
`<div @click="alert(1)">Text</div>`,
`<div v-on:click="alert(1)">Text</div>`,
`<div v-slot="{x = alert(1)}">Text</div>`
]
for (const input of inputs) {
expect(await renderer.init(input, config)).toEqual('<div>Text</div>')
}
})
it('leaves regular content attributes untouched', async () => {
const input = '<a href="/foo" target="_blank" title="Foo" data-id="1">Link</a>'
const result = await renderer.init(input, config)
expect(result).toEqual(input)
})
it('does not sanitize anything when safeHTML is disabled', async () => {
const input = `<tabset><template v-slot:tabs="{x = alert(1)}"><li>Tab</li></template></tabset>`
const result = await renderer.init(input, { ...config, safeHTML: false })
expect(result).toEqual(input)
})
})
Loading…
Cancel
Save