Merge pull request #12 from TauNeutrino/fix-xss-render-history-904859585010159921

🔒 Fix XSS Vulnerability in renderHistory
This commit is contained in:
Michael
2026-03-10 19:47:13 +01:00
committed by GitHub
3 changed files with 243 additions and 109 deletions

175
dist/kantine.bundle.js vendored
View File

@@ -273,87 +273,154 @@ function renderHistory(orders) {
}); });
}); });
content.innerHTML = '';
const sortedYears = Object.keys(groups).sort((a, b) => b - a); const sortedYears = Object.keys(groups).sort((a, b) => b - a);
let html = '';
sortedYears.forEach(yKey => { sortedYears.forEach(yKey => {
const yearGroup = groups[yKey]; const yearGroup = groups[yKey];
html += `<div class="history-year-group"> const yearGroupDiv = document.createElement('div');
<h2 class="history-year-header">${yearGroup.year}</h2>`; yearGroupDiv.className = 'history-year-group';
const yearHeader = document.createElement('h2');
yearHeader.className = 'history-year-header';
yearHeader.textContent = yearGroup.year;
yearGroupDiv.appendChild(yearHeader);
const sortedMonths = Object.keys(yearGroup.months).sort((a, b) => b.localeCompare(a)); const sortedMonths = Object.keys(yearGroup.months).sort((a, b) => b.localeCompare(a));
sortedMonths.forEach(mKey => { sortedMonths.forEach(mKey => {
const monthGroup = yearGroup.months[mKey]; const monthGroup = yearGroup.months[mKey];
html += `<div class="history-month-group"> const monthGroupDiv = document.createElement('div');
<div class="history-month-header" tabindex="0" role="button" aria-expanded="false" title="Klicken, um die Bestellungen für diesen Monat ein-/auszublenden"> monthGroupDiv.className = 'history-month-group';
<div style="display:flex; flex-direction:column; gap:4px;">
<span>${monthGroup.name}</span> const monthHeader = document.createElement('div');
<div class="history-month-summary"> monthHeader.className = 'history-month-header';
<span>${monthGroup.count} Bestellungen &bull; <strong>€${monthGroup.total.toFixed(2)}</strong></span> monthHeader.setAttribute('tabindex', '0');
</div> monthHeader.setAttribute('role', 'button');
</div> monthHeader.setAttribute('aria-expanded', 'false');
<span class="material-icons-round">expand_more</span> monthHeader.setAttribute('title', 'Klicken, um die Bestellungen für diesen Monat ein-/auszublenden');
</div>
<div class="history-month-content">`; const monthHeaderContent = document.createElement('div');
monthHeaderContent.style.display = 'flex';
monthHeaderContent.style.flexDirection = 'column';
monthHeaderContent.style.gap = '4px';
const monthNameSpan = document.createElement('span');
monthNameSpan.textContent = monthGroup.name;
monthHeaderContent.appendChild(monthNameSpan);
const monthSummary = document.createElement('div');
monthSummary.className = 'history-month-summary';
const monthSummarySpan = document.createElement('span');
monthSummarySpan.innerHTML = `${monthGroup.count} Bestellungen &bull; <strong>€${monthGroup.total.toFixed(2)}</strong>`;
monthSummary.appendChild(monthSummarySpan);
monthHeaderContent.appendChild(monthSummary);
monthHeader.appendChild(monthHeaderContent);
const expandIcon = document.createElement('span');
expandIcon.className = 'material-icons-round';
expandIcon.textContent = 'expand_more';
monthHeader.appendChild(expandIcon);
monthHeader.addEventListener('click', () => {
const parentGroup = monthHeader.parentElement;
const isOpen = parentGroup.classList.contains('open');
if (isOpen) {
parentGroup.classList.remove('open');
monthHeader.setAttribute('aria-expanded', 'false');
} else {
parentGroup.classList.add('open');
monthHeader.setAttribute('aria-expanded', 'true');
}
});
monthGroupDiv.appendChild(monthHeader);
const monthContentDiv = document.createElement('div');
monthContentDiv.className = 'history-month-content';
const sortedKWs = Object.keys(monthGroup.weeks).sort((a, b) => parseInt(b) - parseInt(a)); const sortedKWs = Object.keys(monthGroup.weeks).sort((a, b) => parseInt(b) - parseInt(a));
sortedKWs.forEach(kw => { sortedKWs.forEach(kw => {
const week = monthGroup.weeks[kw]; const week = monthGroup.weeks[kw];
html += `<div class="history-week-group"> const weekGroupDiv = document.createElement('div');
<div class="history-week-header"> weekGroupDiv.className = 'history-week-group';
<strong>${week.label}</strong>
<span>${week.count} Bestellungen &bull; <strong>€${week.total.toFixed(2)}</strong></span> const weekHeader = document.createElement('div');
</div>`; weekHeader.className = 'history-week-header';
const weekLabel = document.createElement('strong');
weekLabel.textContent = week.label;
weekHeader.appendChild(weekLabel);
const weekSummary = document.createElement('span');
weekSummary.innerHTML = `${week.count} Bestellungen &bull; <strong>€${week.total.toFixed(2)}</strong>`;
weekHeader.appendChild(weekSummary);
weekGroupDiv.appendChild(weekHeader);
week.items.forEach(item => { week.items.forEach(item => {
const dateObj = new Date(item.date); const dateObj = new Date(item.date);
const dayStr = dateObj.toLocaleDateString('de-AT', { weekday: 'short', day: '2-digit', month: '2-digit' }); const dayStr = dateObj.toLocaleDateString('de-AT', { weekday: 'short', day: '2-digit', month: '2-digit' });
let statusBadge = ''; const historyItem = document.createElement('div');
historyItem.className = 'history-item';
if (item.state === 9) { if (item.state === 9) {
statusBadge = '<span class="history-item-status">Storniert</span>'; historyItem.classList.add('history-item-cancelled');
} else if (item.state === 8) {
statusBadge = '<span class="history-item-status">Abgeschlossen</span>';
} else {
statusBadge = '<span class="history-item-status">Übertragen</span>';
} }
html += ` const dateDiv = document.createElement('div');
<div class="history-item ${item.state === 9 ? 'history-item-cancelled' : ''}"> dateDiv.style.fontSize = '0.85rem';
<div style="font-size: 0.85rem; color: var(--text-secondary);">${dayStr}</div> dateDiv.style.color = 'var(--text-secondary)';
<div class="history-item-details"> dateDiv.textContent = dayStr;
<span class="history-item-name">${(0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .escapeHtml */ .ZD)(item.name)}</span> historyItem.appendChild(dateDiv);
<div>${statusBadge}</div>
</div> const detailsDiv = document.createElement('div');
<div class="history-item-price ${item.state === 9 ? 'history-item-price-cancelled' : ''}">€${item.price.toFixed(2)}</div> detailsDiv.className = 'history-item-details';
</div>`;
const nameSpan = document.createElement('span');
nameSpan.className = 'history-item-name';
nameSpan.textContent = item.name;
detailsDiv.appendChild(nameSpan);
const statusDiv = document.createElement('div');
const statusSpan = document.createElement('span');
statusSpan.className = 'history-item-status';
if (item.state === 9) {
statusSpan.textContent = 'Storniert';
} else if (item.state === 8) {
statusSpan.textContent = 'Abgeschlossen';
} else {
statusSpan.textContent = 'Übertragen';
}
statusDiv.appendChild(statusSpan);
detailsDiv.appendChild(statusDiv);
historyItem.appendChild(detailsDiv);
const priceDiv = document.createElement('div');
priceDiv.className = 'history-item-price';
if (item.state === 9) {
priceDiv.classList.add('history-item-price-cancelled');
}
priceDiv.textContent = `${item.price.toFixed(2)}`;
historyItem.appendChild(priceDiv);
weekGroupDiv.appendChild(historyItem);
}); });
html += `</div>`;
monthContentDiv.appendChild(weekGroupDiv);
}); });
html += `</div></div>`;
monthGroupDiv.appendChild(monthContentDiv);
yearGroupDiv.appendChild(monthGroupDiv);
}); });
html += `</div>`;
});
content.innerHTML = html; content.appendChild(yearGroupDiv);
const monthHeaders = content.querySelectorAll('.history-month-header');
monthHeaders.forEach(header => {
header.addEventListener('click', () => {
const parentGroup = header.parentElement;
const isOpen = parentGroup.classList.contains('open');
if (isOpen) {
parentGroup.classList.remove('open');
header.setAttribute('aria-expanded', 'false');
} else {
parentGroup.classList.add('open');
header.setAttribute('aria-expanded', 'true');
}
});
}); });
} }

2
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{ {
"name": "kantine-wrapper", "name": "app",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@@ -241,87 +241,154 @@ export function renderHistory(orders) {
}); });
}); });
content.innerHTML = '';
const sortedYears = Object.keys(groups).sort((a, b) => b - a); const sortedYears = Object.keys(groups).sort((a, b) => b - a);
let html = '';
sortedYears.forEach(yKey => { sortedYears.forEach(yKey => {
const yearGroup = groups[yKey]; const yearGroup = groups[yKey];
html += `<div class="history-year-group"> const yearGroupDiv = document.createElement('div');
<h2 class="history-year-header">${yearGroup.year}</h2>`; yearGroupDiv.className = 'history-year-group';
const yearHeader = document.createElement('h2');
yearHeader.className = 'history-year-header';
yearHeader.textContent = yearGroup.year;
yearGroupDiv.appendChild(yearHeader);
const sortedMonths = Object.keys(yearGroup.months).sort((a, b) => b.localeCompare(a)); const sortedMonths = Object.keys(yearGroup.months).sort((a, b) => b.localeCompare(a));
sortedMonths.forEach(mKey => { sortedMonths.forEach(mKey => {
const monthGroup = yearGroup.months[mKey]; const monthGroup = yearGroup.months[mKey];
html += `<div class="history-month-group"> const monthGroupDiv = document.createElement('div');
<div class="history-month-header" tabindex="0" role="button" aria-expanded="false" title="Klicken, um die Bestellungen für diesen Monat ein-/auszublenden"> monthGroupDiv.className = 'history-month-group';
<div style="display:flex; flex-direction:column; gap:4px;">
<span>${monthGroup.name}</span> const monthHeader = document.createElement('div');
<div class="history-month-summary"> monthHeader.className = 'history-month-header';
<span>${monthGroup.count} Bestellungen &bull; <strong>€${monthGroup.total.toFixed(2)}</strong></span> monthHeader.setAttribute('tabindex', '0');
</div> monthHeader.setAttribute('role', 'button');
</div> monthHeader.setAttribute('aria-expanded', 'false');
<span class="material-icons-round">expand_more</span> monthHeader.setAttribute('title', 'Klicken, um die Bestellungen für diesen Monat ein-/auszublenden');
</div>
<div class="history-month-content">`; const monthHeaderContent = document.createElement('div');
monthHeaderContent.style.display = 'flex';
monthHeaderContent.style.flexDirection = 'column';
monthHeaderContent.style.gap = '4px';
const monthNameSpan = document.createElement('span');
monthNameSpan.textContent = monthGroup.name;
monthHeaderContent.appendChild(monthNameSpan);
const monthSummary = document.createElement('div');
monthSummary.className = 'history-month-summary';
const monthSummarySpan = document.createElement('span');
monthSummarySpan.innerHTML = `${monthGroup.count} Bestellungen &bull; <strong>€${monthGroup.total.toFixed(2)}</strong>`;
monthSummary.appendChild(monthSummarySpan);
monthHeaderContent.appendChild(monthSummary);
monthHeader.appendChild(monthHeaderContent);
const expandIcon = document.createElement('span');
expandIcon.className = 'material-icons-round';
expandIcon.textContent = 'expand_more';
monthHeader.appendChild(expandIcon);
monthHeader.addEventListener('click', () => {
const parentGroup = monthHeader.parentElement;
const isOpen = parentGroup.classList.contains('open');
if (isOpen) {
parentGroup.classList.remove('open');
monthHeader.setAttribute('aria-expanded', 'false');
} else {
parentGroup.classList.add('open');
monthHeader.setAttribute('aria-expanded', 'true');
}
});
monthGroupDiv.appendChild(monthHeader);
const monthContentDiv = document.createElement('div');
monthContentDiv.className = 'history-month-content';
const sortedKWs = Object.keys(monthGroup.weeks).sort((a, b) => parseInt(b) - parseInt(a)); const sortedKWs = Object.keys(monthGroup.weeks).sort((a, b) => parseInt(b) - parseInt(a));
sortedKWs.forEach(kw => { sortedKWs.forEach(kw => {
const week = monthGroup.weeks[kw]; const week = monthGroup.weeks[kw];
html += `<div class="history-week-group"> const weekGroupDiv = document.createElement('div');
<div class="history-week-header"> weekGroupDiv.className = 'history-week-group';
<strong>${week.label}</strong>
<span>${week.count} Bestellungen &bull; <strong>€${week.total.toFixed(2)}</strong></span> const weekHeader = document.createElement('div');
</div>`; weekHeader.className = 'history-week-header';
const weekLabel = document.createElement('strong');
weekLabel.textContent = week.label;
weekHeader.appendChild(weekLabel);
const weekSummary = document.createElement('span');
weekSummary.innerHTML = `${week.count} Bestellungen &bull; <strong>€${week.total.toFixed(2)}</strong>`;
weekHeader.appendChild(weekSummary);
weekGroupDiv.appendChild(weekHeader);
week.items.forEach(item => { week.items.forEach(item => {
const dateObj = new Date(item.date); const dateObj = new Date(item.date);
const dayStr = dateObj.toLocaleDateString('de-AT', { weekday: 'short', day: '2-digit', month: '2-digit' }); const dayStr = dateObj.toLocaleDateString('de-AT', { weekday: 'short', day: '2-digit', month: '2-digit' });
let statusBadge = ''; const historyItem = document.createElement('div');
historyItem.className = 'history-item';
if (item.state === 9) { if (item.state === 9) {
statusBadge = '<span class="history-item-status">Storniert</span>'; historyItem.classList.add('history-item-cancelled');
} else if (item.state === 8) {
statusBadge = '<span class="history-item-status">Abgeschlossen</span>';
} else {
statusBadge = '<span class="history-item-status">Übertragen</span>';
} }
html += ` const dateDiv = document.createElement('div');
<div class="history-item ${item.state === 9 ? 'history-item-cancelled' : ''}"> dateDiv.style.fontSize = '0.85rem';
<div style="font-size: 0.85rem; color: var(--text-secondary);">${dayStr}</div> dateDiv.style.color = 'var(--text-secondary)';
<div class="history-item-details"> dateDiv.textContent = dayStr;
<span class="history-item-name">${escapeHtml(item.name)}</span> historyItem.appendChild(dateDiv);
<div>${statusBadge}</div>
</div> const detailsDiv = document.createElement('div');
<div class="history-item-price ${item.state === 9 ? 'history-item-price-cancelled' : ''}">€${item.price.toFixed(2)}</div> detailsDiv.className = 'history-item-details';
</div>`;
const nameSpan = document.createElement('span');
nameSpan.className = 'history-item-name';
nameSpan.textContent = item.name;
detailsDiv.appendChild(nameSpan);
const statusDiv = document.createElement('div');
const statusSpan = document.createElement('span');
statusSpan.className = 'history-item-status';
if (item.state === 9) {
statusSpan.textContent = 'Storniert';
} else if (item.state === 8) {
statusSpan.textContent = 'Abgeschlossen';
} else {
statusSpan.textContent = 'Übertragen';
}
statusDiv.appendChild(statusSpan);
detailsDiv.appendChild(statusDiv);
historyItem.appendChild(detailsDiv);
const priceDiv = document.createElement('div');
priceDiv.className = 'history-item-price';
if (item.state === 9) {
priceDiv.classList.add('history-item-price-cancelled');
}
priceDiv.textContent = `${item.price.toFixed(2)}`;
historyItem.appendChild(priceDiv);
weekGroupDiv.appendChild(historyItem);
}); });
html += `</div>`;
monthContentDiv.appendChild(weekGroupDiv);
}); });
html += `</div></div>`;
monthGroupDiv.appendChild(monthContentDiv);
yearGroupDiv.appendChild(monthGroupDiv);
}); });
html += `</div>`;
});
content.innerHTML = html; content.appendChild(yearGroupDiv);
const monthHeaders = content.querySelectorAll('.history-month-header');
monthHeaders.forEach(header => {
header.addEventListener('click', () => {
const parentGroup = header.parentElement;
const isOpen = parentGroup.classList.contains('open');
if (isOpen) {
parentGroup.classList.remove('open');
header.setAttribute('aria-expanded', 'false');
} else {
parentGroup.classList.add('open');
header.setAttribute('aria-expanded', 'true');
}
});
}); });
} }