Merge pull request #4 from TauNeutrino/fix-xss-vulnerability-2050985831484711700
🔒 security: fix XSS vulnerabilities in UI helpers and actions
This commit is contained in:
@@ -899,7 +899,7 @@ export async function loadMenuDataFromAPI() {
|
|||||||
import('./ui_helpers.js').then(uiHelpers => {
|
import('./ui_helpers.js').then(uiHelpers => {
|
||||||
uiHelpers.showErrorModal(
|
uiHelpers.showErrorModal(
|
||||||
'Keine Verbindung',
|
'Keine Verbindung',
|
||||||
`Die Menüdaten konnten nicht geladen werden. Möglicherweise besteht keine Verbindung zur API oder zur Bessa-Webseite.<br><br><small style="color:var(--text-secondary)">${error.message}</small>`,
|
`Die Menüdaten konnten nicht geladen werden. Möglicherweise besteht keine Verbindung zur API oder zur Bessa-Webseite.<br><br><small style="color:var(--text-secondary)">${escapeHtml(error.message)}</small>`,
|
||||||
'Zur Original-Seite',
|
'Zur Original-Seite',
|
||||||
'https://web.bessa.app/knapp-kantine'
|
'https://web.bessa.app/knapp-kantine'
|
||||||
);
|
);
|
||||||
@@ -947,7 +947,7 @@ export function showToast(message, type = 'info') {
|
|||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.className = `toast toast-${type}`;
|
toast.className = `toast toast-${type}`;
|
||||||
const icon = type === 'success' ? 'check_circle' : type === 'error' ? 'error' : 'info';
|
const icon = type === 'success' ? 'check_circle' : type === 'error' ? 'error' : 'info';
|
||||||
toast.innerHTML = `<span class="material-icons-round">${icon}</span><span>${message}</span>`;
|
toast.innerHTML = `<span class="material-icons-round">${icon}</span><span>${escapeHtml(message)}</span>`;
|
||||||
container.appendChild(toast);
|
container.appendChild(toast);
|
||||||
requestAnimationFrame(() => toast.classList.add('show'));
|
requestAnimationFrame(() => toast.classList.add('show'));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -516,12 +516,12 @@ export function openVersionMenu() {
|
|||||||
|
|
||||||
let action = '';
|
let action = '';
|
||||||
if (!isCurrent) {
|
if (!isCurrent) {
|
||||||
action = `<a href="${v.url}" target="_blank" class="install-link" title="${v.tag} installieren">Installieren</a>`;
|
action = `<a href="${escapeHtml(v.url)}" target="_blank" class="install-link" title="${escapeHtml(v.tag)} installieren">Installieren</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
<strong>${v.tag}</strong>
|
<strong>${escapeHtml(v.tag)}</strong>
|
||||||
${badge}
|
${badge}
|
||||||
</div>
|
</div>
|
||||||
${action}
|
${action}
|
||||||
@@ -554,7 +554,7 @@ export function openVersionMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`;
|
container.innerHTML = `<p style="color:#e94560;">Fehler: ${escapeHtml(e.message)}</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,7 +661,7 @@ export function showErrorModal(title, htmlContent, btnText, url) {
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 style="color: var(--error-color); display: flex; align-items: center; gap: 10px;">
|
<h2 style="color: var(--error-color); display: flex; align-items: center; gap: 10px;">
|
||||||
<span class="material-icons-round">signal_wifi_off</span>
|
<span class="material-icons-round">signal_wifi_off</span>
|
||||||
${title}
|
${escapeHtml(title)}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 20px;">
|
<div style="padding: 20px;">
|
||||||
@@ -682,7 +682,7 @@ export function showErrorModal(title, htmlContent, btnText, url) {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: transform 0.1s;
|
transition: transform 0.1s;
|
||||||
">
|
">
|
||||||
${btnText}
|
${escapeHtml(btnText)}
|
||||||
<span class="material-icons-round">open_in_new</span>
|
<span class="material-icons-round">open_in_new</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
137
tests/repro_vulnerability.js
Normal file
137
tests/repro_vulnerability.js
Normal file
@@ -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('<img src=x onerror=alert(1)>')) {
|
||||||
|
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('<img src=x onerror=alert(1)>')),
|
||||||
|
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('<img src=x onerror=alert(1)>');
|
||||||
|
|
||||||
|
console.log("Testing showErrorModal...");
|
||||||
|
sandbox.showErrorModal('<img src=x onerror=alert(1)>', 'safe content', '<img src=x onerror=alert(1)>', '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: '<img src=x onerror=alert(1)>',
|
||||||
|
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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user