From a2b2ec227f1d1a7e11b3ef65b64415aab1998ec4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:37:54 +0000 Subject: [PATCH] security: escape dynamic content in innerHTML to prevent XSS This commit addresses several XSS vulnerabilities by ensuring that dynamic data from external APIs (GitHub) and error messages are properly escaped before being rendered via innerHTML. Affected areas: - openVersionMenu error handling and version list - showErrorModal title and button text - showToast message content All changes were verified with a reproduction test case. Co-authored-by: TauNeutrino <1600410+TauNeutrino@users.noreply.github.com> --- src/actions.js | 4 +- src/ui_helpers.js | 10 +-- tests/repro_vulnerability.js | 137 +++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 tests/repro_vulnerability.js diff --git a/src/actions.js b/src/actions.js index bb9e01e..8efa5a3 100644 --- a/src/actions.js +++ b/src/actions.js @@ -899,7 +899,7 @@ export async function loadMenuDataFromAPI() { import('./ui_helpers.js').then(uiHelpers => { uiHelpers.showErrorModal( 'Keine Verbindung', - `Die Menüdaten konnten nicht geladen werden. Möglicherweise besteht keine Verbindung zur API oder zur Bessa-Webseite.

${error.message}`, + `Die Menüdaten konnten nicht geladen werden. Möglicherweise besteht keine Verbindung zur API oder zur Bessa-Webseite.

${escapeHtml(error.message)}`, 'Zur Original-Seite', 'https://web.bessa.app/knapp-kantine' ); @@ -947,7 +947,7 @@ export function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; const icon = type === 'success' ? 'check_circle' : type === 'error' ? 'error' : 'info'; - toast.innerHTML = `${icon}${message}`; + toast.innerHTML = `${icon}${escapeHtml(message)}`; container.appendChild(toast); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { diff --git a/src/ui_helpers.js b/src/ui_helpers.js index 9469dff..413798b 100644 --- a/src/ui_helpers.js +++ b/src/ui_helpers.js @@ -516,12 +516,12 @@ export function openVersionMenu() { let action = ''; if (!isCurrent) { - action = `Installieren`; + action = `Installieren`; } li.innerHTML = `
- ${v.tag} + ${escapeHtml(v.tag)} ${badge}
${action} @@ -554,7 +554,7 @@ export function openVersionMenu() { } } catch (e) { - container.innerHTML = `

Fehler: ${e.message}

`; + container.innerHTML = `

Fehler: ${escapeHtml(e.message)}

`; } } @@ -661,7 +661,7 @@ export function showErrorModal(title, htmlContent, btnText, url) {
@@ -682,7 +682,7 @@ export function showErrorModal(title, htmlContent, btnText, url) { justify-content: center; transition: transform 0.1s; "> - ${btnText} + ${escapeHtml(btnText)} open_in_new
diff --git a/tests/repro_vulnerability.js b/tests/repro_vulnerability.js new file mode 100644 index 0000000..78c2eb8 --- /dev/null +++ b/tests/repro_vulnerability.js @@ -0,0 +1,137 @@ +const fs = require('fs'); +const vm = require('vm'); +const path = require('path'); + +console.log("=== Running Vulnerability Reproduction Tests ==="); + +// Mock DOM +const createMockElement = (id = 'mock') => { + const el = { + id, + classList: { add: () => { }, remove: () => { }, contains: () => false }, + _innerHTML: '', + get innerHTML() { return this._innerHTML; }, + set innerHTML(val) { + this._innerHTML = val; + // Check for XSS + if (val.includes('')) { + console.error(`❌ VULNERABILITY DETECTED in ${id}: XSS payload found in innerHTML!`); + console.error(`Payload: ${val}`); + process.exit(1); + } + }, + _textContent: '', + get textContent() { return this._textContent; }, + set textContent(val) { this._textContent = val; }, + value: '', + style: { cssText: '', display: '' }, + addEventListener: () => { }, + removeEventListener: () => { }, + appendChild: (child) => { }, + removeChild: () => { }, + querySelector: (sel) => createMockElement(sel), + querySelectorAll: () => [createMockElement()], + getAttribute: () => '', + setAttribute: () => { }, + remove: () => { }, + dataset: {} + }; + return el; +}; + +const sandbox = { + console: console, + document: { + body: createMockElement('body'), + createElement: (tag) => createMockElement(tag), + getElementById: (id) => createMockElement(id), + querySelector: (sel) => createMockElement(sel), + }, + localStorage: { + getItem: () => null, + setItem: () => { }, + removeItem: () => { } + }, + fetch: () => Promise.reject(new Error('')), + setTimeout: (cb) => cb(), + setInterval: () => { }, + requestAnimationFrame: (cb) => cb(), + Date: Date, + Notification: { permission: 'denied', requestPermission: () => { } }, + window: { location: { href: '' } }, + crypto: { randomUUID: () => '1234' } +}; + +// Load utils.js (for escapeHtml if needed) +const utilsCode = fs.readFileSync(path.join(__dirname, '../src/utils.js'), 'utf8') + .replace(/export /g, '') + .replace(/import .*? from .*?;/g, ''); + +// Load constants.js +const constantsCode = fs.readFileSync(path.join(__dirname, '../src/constants.js'), 'utf8') + .replace(/export /g, ''); + +// Load ui_helpers.js +const uiHelpersCode = fs.readFileSync(path.join(__dirname, '../src/ui_helpers.js'), 'utf8') + .replace(/export /g, '') + .replace(/import .*? from .*?;/g, ''); + +// Load actions.js +const actionsCode = fs.readFileSync(path.join(__dirname, '../src/actions.js'), 'utf8') + .replace(/export /g, '') + .replace(/import .*? from .*?;/g, ''); + +vm.createContext(sandbox); +vm.runInContext(utilsCode, sandbox); +vm.runInContext(constantsCode, sandbox); +// Mock state +vm.runInContext(` + var authToken = 'mock-token'; + var currentUser = 'mock-user'; + var orderMap = new Map(); + var userFlags = new Set(); + var highlightTags = []; + var allWeeks = []; + var currentWeekNumber = 1; + var currentYear = 2024; + var displayMode = 'this-week'; + var langMode = 'de'; +`, sandbox); +vm.runInContext(uiHelpersCode, sandbox); +vm.runInContext(actionsCode, sandbox); + +async function runTests() { + console.log("Testing openVersionMenu error handling..."); + try { + await sandbox.openVersionMenu(); + } catch (e) {} + + console.log("Testing showToast..."); + sandbox.showToast(''); + + console.log("Testing showErrorModal..."); + sandbox.showErrorModal('', 'safe content', '', 'http://example.com'); + + console.log("Testing openVersionMenu version list rendering..."); + // Mock successful fetch but with malicious data + sandbox.fetch = () => Promise.resolve({ + ok: true, + json: () => Promise.resolve([ + { + tag: '', + name: 'malicious', + url: 'javascript:alert(1)', + body: 'malicious body' + } + ]) + }); + + await sandbox.openVersionMenu(); + + console.log("All tests finished (if you see this, no vulnerability was detected by the check)."); +} + +runTests().catch(err => { + console.error("Test execution failed:", err); + process.exit(1); +});