'use strict'; // Regression for GHSA-8r5p-4q9j-f5jx: Stored XSS in HTML board exports through a // card-title second parse. // // The exported index.html embeds a click handler that opens a per-card modal. It // read the card title/body via .textContent (which DECODES HTML entities) and then // concatenated those values into `content.innerHTML` — a SECOND parse that revived // entity-encoded markup (e.g. a card titled `<img src=x onerror=...>`) and // executed it when a recipient clicked the card in the export. The fix builds the // modal with DOM nodes and assigns the title/body through textContent, never // innerHTML. // // exportHTML.js is a Blaze/ESM client module, so this guards the SOURCE of the // injected handler: the untrusted values (titleText/allText) must reach only // textContent, and must never be concatenated into an innerHTML assignment. // Run: node tests/exportHTMLXss.test.cjs const assert = require('assert'); const fs = require('fs'); const path = require('path'); let passed = 0; function check(name, fn) { fn(); passed += 1; console.log(' ok -', name); } const src = fs.readFileSync( path.join(__dirname, '..', 'client', 'lib', 'exportHTML.js'), 'utf8'); check('the card title/body are inserted via textContent (inert), not innerHTML', () => { assert.ok(/titleEl\.textContent = titleText/.test(src), 'the card title must be assigned through textContent'); assert.ok(/bodyEl\.textContent = allText/.test(src), 'the card body must be assigned through textContent'); }); check('titleText / allText never reach an innerHTML assignment (the second parse)', () => { // Every `X.innerHTML = ...` assignment in the file must not include the // entity-decoded card values on its right-hand side. const innerHtmlAssigns = src.match(/\.innerHTML\s*=\s*[^\n;]+/g) || []; for (const line of innerHtmlAssigns) { assert.ok(!/titleText|allText/.test(line), `titleText/allText must not flow into innerHTML: ${line}`); } }); check('the old vulnerable concatenation is gone', () => { assert.ok(!/content\.innerHTML\s*=\s*'