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
+7 -2
View File
@@ -147,14 +147,19 @@ router.get('/export', (req, res) => {
`).all(from, to);
const header = 'Datum,Titel,Betrag,Kategorie,Wiederkehrend,Erstellt von\n';
const csvSafe = (val) => {
let s = String(val || '').replace(/"/g, '""');
if (/^[=+\-@\t\r]/.test(s)) s = "'" + s;
return `"${s}"`;
};
const rows = entries.map((e) =>
[
e.date,
`"${(e.title || '').replace(/"/g, '""')}"`,
csvSafe(e.title),
e.amount.toFixed(2).replace('.', ','),
e.category,
e.is_recurring ? 'Ja' : 'Nein',
`"${(e.creator_name || '').replace(/"/g, '""')}"`,
csvSafe(e.creator_name),
].join(',')
).join('\n');
+11 -4
View File
@@ -206,7 +206,7 @@ router.get('/upcoming', (req, res) => {
*/
router.get('/google/auth', requireAdmin, (req, res) => {
try {
const url = googleCalendar.getAuthUrl();
const url = googleCalendar.getAuthUrl(req.session);
if (!url) return res.status(503).json({ error: 'Google nicht konfiguriert.', code: 503 });
res.redirect(url);
} catch (err) {
@@ -222,10 +222,17 @@ router.get('/google/auth', requireAdmin, (req, res) => {
*/
router.get('/google/callback', async (req, res) => {
try {
const { code, error } = req.query;
const { code, error, state } = req.query;
if (error) return res.redirect('/settings?sync_error=google');
if (!code) return res.status(400).json({ error: 'Kein Code erhalten.', code: 400 });
// OAuth CSRF-Schutz: state-Parameter validieren
if (!state || !req.session.googleOAuthState || state !== req.session.googleOAuthState) {
console.error('[calendar/google/callback] OAuth state mismatch');
return res.redirect('/settings?sync_error=google');
}
delete req.session.googleOAuthState;
await googleCalendar.handleCallback(code);
// Initialen Sync im Hintergrund starten (kein await - Redirect soll sofort erfolgen)
@@ -243,7 +250,7 @@ router.get('/google/callback', async (req, res) => {
* Manueller Sync-Trigger.
* Response: { ok: true, lastSync: string }
*/
router.post('/google/sync', async (req, res) => {
router.post('/google/sync', requireAdmin, async (req, res) => {
try {
await googleCalendar.sync();
const { lastSync } = googleCalendar.getStatus();
@@ -304,7 +311,7 @@ router.get('/apple/status', (req, res) => {
* Manueller Sync-Trigger.
* Response: { ok: true, lastSync: string }
*/
router.post('/apple/sync', async (req, res) => {
router.post('/apple/sync', requireAdmin, async (req, res) => {
try {
await appleCalendar.sync();
const { lastSync } = appleCalendar.getStatus();