71 lines
3.7 KiB
JavaScript
71 lines
3.7 KiB
JavaScript
const assert = require('assert')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const vm = require('vm')
|
|
const babel = require('@babel/core')
|
|
const compiler = require('vue-template-compiler')
|
|
const { JSDOM } = require('jsdom')
|
|
const browser = new JSDOM('', { url: 'https://erp.example.test' }).window
|
|
const purifier = require('dompurify')(browser)
|
|
|
|
function loadSource (relative, dependencies = {}) {
|
|
const filename = path.join(__dirname, '..', relative)
|
|
const source = fs.readFileSync(filename, 'utf8')
|
|
const script = relative.endsWith('.vue') ? compiler.parseComponent(source).script.content : source
|
|
const transformed = babel.transformSync(script, {
|
|
babelrc: false,
|
|
configFile: false,
|
|
plugins: ['@babel/plugin-transform-modules-commonjs']
|
|
}).code
|
|
const exports = {}
|
|
vm.runInNewContext(transformed, {
|
|
exports,
|
|
require: name => {
|
|
assert.ok(Object.prototype.hasOwnProperty.call(dependencies, name), `Unexpected import ${name}`)
|
|
return dependencies[name]
|
|
},
|
|
window: browser,
|
|
console: { log () {}, warn () {} }
|
|
}, { filename })
|
|
return exports
|
|
}
|
|
|
|
const menu = loadSource('src/utils/menuUrl.js')
|
|
browser._CONFIG = { domianURL: '/apis' }
|
|
assert.strictEqual(menu.resolveMenuUrl('{{ window._CONFIG.domianURL }}/report', browser), '/apis/report')
|
|
assert.strictEqual(menu.resolveMenuUrl("{{ window._CONFIG['domianURL'] }}/report", browser), '/apis/report')
|
|
assert.strictEqual(menu.resolveMenuUrl('{{ window.location.origin }}/report', browser), 'https://erp.example.test/report')
|
|
assert.strictEqual(menu.resolveMenuUrl('/dashboard/analysis', browser), '/dashboard/analysis')
|
|
const attack = '{{ window.compromised = true }}'
|
|
assert.strictEqual(menu.resolveMenuUrl(attack, browser), attack)
|
|
assert.strictEqual(browser.compromised, undefined)
|
|
assert.strictEqual(menu.resolveMenuUrl('{{ constructor.constructor("alert(1)")() }}', browser),
|
|
'{{ constructor.constructor("alert(1)")() }}')
|
|
console.log('PASS: supported menu placeholders and malicious expressions')
|
|
|
|
const announcement = loadSource('src/components/tools/ShowAnnouncement.vue', { dompurify: purifier }).default
|
|
const safeContent = content => announcement.computed.safeContent.call({ record: { msgContent: content } })
|
|
assert.strictEqual(safeContent('<p>中文公告 <strong>加粗</strong></p>'), '<p>中文公告 <strong>加粗</strong></p>')
|
|
assert.strictEqual(safeContent(null), '')
|
|
const sanitized = safeContent('<p>公告</p><script>alert(1)</script><img src=x onerror=alert(1)>' +
|
|
'<a href="javascript:alert(1)">link</a><svg onload=alert(1)></svg><iframe srcdoc="bad"></iframe>')
|
|
assert.ok(sanitized.includes('<p>公告</p>'))
|
|
assert.ok(!/script|onerror|onload|javascript:|svg|iframe/i.test(sanitized))
|
|
const vueSource = fs.readFileSync(path.join(__dirname, '../src/components/tools/ShowAnnouncement.vue'), 'utf8')
|
|
assert.ok(compiler.compile(compiler.parseComponent(vueSource).template.content).render.includes('safeContent'))
|
|
console.log('PASS: rich-text formatting retained and XSS payloads removed')
|
|
|
|
const notice = loadSource('src/components/tools/HeaderNotice.vue', {
|
|
'@/api/manage': {}, './ShowAnnouncement': {}, '@/store/': {}, './DynamicNotice': {}, '@/views/system/MsgList': {}
|
|
}).default
|
|
let updates = 0
|
|
const receive = data => notice.methods.websocketOnmessage.call({ loadData () { updates++ } }, { data })
|
|
receive('{"cmd":"topic"}')
|
|
receive('{"cmd":"user"}')
|
|
assert.strictEqual(updates, 2)
|
|
for (const input of ['null', '[]', '42', 'invalid', '{"cmd":"heartbeat"}', '(window.compromised=true)']) receive(input)
|
|
assert.strictEqual(updates, 2)
|
|
assert.strictEqual(browser.compromised, undefined)
|
|
browser.close()
|
|
console.log('PASS: notification commands preserved and executable/non-object payloads ignored')
|