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