fix(security): address critical and high findings from security audit

Fix stored XSS in tasks (titles/subtasks) and settings (member list)
by applying escHtml(). Harden trust proxy to loopback default, add
OAuth state parameter for Google Calendar CSRF protection, sanitize
CSV export against formula injection, invalidate sessions on user
deletion, restrict usernames to alphanumeric chars, and require admin
role for calendar sync triggers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas
2026-04-03 17:28:36 +02:00
parent 1122bd269b
commit 3d2604bab9
10 changed files with 96 additions and 20 deletions
+13 -4
View File
@@ -488,15 +488,24 @@ function bindDeleteButtons(container, user) {
// Helfer
// --------------------------------------------------------
function escHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function memberHtml(u) {
return `
<li class="settings-member" data-id="${u.id}">
<div class="settings-avatar settings-avatar--sm" style="background:${u.avatar_color}">${initials(u.display_name)}</div>
<div class="settings-avatar settings-avatar--sm" style="background:${escHtml(u.avatar_color)}">${initials(u.display_name)}</div>
<div class="settings-member__info">
<span class="settings-member__name">${u.display_name}</span>
<span class="settings-member__meta">@${u.username} · ${u.role === 'admin' ? t('settings.roleAdmin') : t('settings.roleMember')}</span>
<span class="settings-member__name">${escHtml(u.display_name)}</span>
<span class="settings-member__meta">@${escHtml(u.username)} · ${u.role === 'admin' ? t('settings.roleAdmin') : t('settings.roleMember')}</span>
</div>
<button class="btn btn--icon btn--danger-outline" data-delete-user="${u.id}" data-name="${u.display_name}" aria-label="${u.display_name} ${t('settings.deleteMemberLabel')}" title="${t('settings.deleteMemberLabel')}">
<button class="btn btn--icon btn--danger-outline" data-delete-user="${u.id}" data-name="${escHtml(u.display_name)}" aria-label="${escHtml(u.display_name)} ${t('settings.deleteMemberLabel')}" title="${t('settings.deleteMemberLabel')}">
<i data-lucide="trash-2" aria-hidden="true"></i>
</button>
</li>
+12 -3
View File
@@ -10,6 +10,15 @@ import { openModal as openSharedModal, closeModal, wireBlurValidation, btnSucces
import { stagger, vibrate } from '/utils/ux.js';
import { t, formatDate } from '/i18n.js';
function escHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// --------------------------------------------------------
// Konstanten
// --------------------------------------------------------
@@ -155,7 +164,7 @@ function renderTaskCard(task, opts = {}) {
data-status="${s.status}" aria-label="${t('tasks.subtaskMarkDone', { title: s.title })}">
${s.status === 'done' ? '<i data-lucide="check" style="width:10px;height:10px;color:#fff" aria-hidden="true"></i>' : ''}
</button>
<span class="subtask-item__title">${s.title}</span>
<span class="subtask-item__title">${escHtml(s.title)}</span>
</div>`).join('')
: '';
@@ -170,7 +179,7 @@ function renderTaskCard(task, opts = {}) {
<div class="task-card__body">
<div class="task-card__title" data-action="open-task" data-id="${task.id}">
${task.title}
${escHtml(task.title)}
</div>
<div class="task-card__meta">
${renderPriorityBadge(task.priority)}
@@ -504,7 +513,7 @@ function renderKanbanCard(task) {
return `
<div class="kanban-card ${task.status === 'done' ? 'kanban-card--done' : ''}"
data-task-id="${task.id}" draggable="true">
<div class="kanban-card__title">${task.title}</div>
<div class="kanban-card__title">${escHtml(task.title)}</div>
<div class="kanban-card__meta">
${renderPriorityBadge(task.priority)}
${due ? `<span class="due-date ${due.cls}"><i data-lucide="clock" style="width:10px;height:10px" aria-hidden="true"></i> ${due.label}</span>` : ''}