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('
中文公告 加粗
'), '中文公告 加粗
') assert.strictEqual(safeContent(null), '') const sanitized = safeContent('公告
公告
')) 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')