Files
SMT-ERP/smtERP-web/tests/security.cjs
2026-10-13 16:15:00 +08:00

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')