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); +});