Reviewed-on: #14 Co-authored-by: Patrick Müller <mail@pmueller.me> Co-committed-by: Patrick Müller <mail@pmueller.me>
3.9 KiB
Deferred Security Issues
These items were identified during a security review on 2026-05-02 and consciously deferred. Must be addressed before opening the application to a larger or public userbase.
1. Session credentials in URL query parameters (logged-in users) — CLOSED 2026-09-06
Files: src/models/calendar/events/events.router.ts — all GET/PUT/DELETE handlers
sessionId and sessionKey were read from query parameters, which meant they appeared in
server access logs, browser history, proxy logs, and Referer headers.
Fixed by step 4 of docs/calendar-auth-migration.md: the calendar's write routes now sit
behind requireAppAccess('calendar') against the better-auth session cookie, and the read
routes resolve the same cookie optionally. No route reads sessionId/sessionKey any more,
and the Angular frontend sends withCredentials instead of appending them to every URL. That
closed the item outright rather than moving the credential somewhere safer.
Two things this did not change, both deliberate:
- The shared calendar
passwordparameter stays. An iCal client cannot send a cookie, so this is the one caller that genuinely needs a credential in the URL. It grants read access to one calendar and nothing else -test/calendar/events.router.test.tspins that it can never be used to write. - The legacy
/calendar/users/*routes still exist. Nothing calls them any more, and a legacy session they mint no longer opens anything, but they are still live password-accepting endpoints. Step 5 removes them.
2. No event ownership check
Files: src/models/calendar/events/events.router.ts
PUT /:eventId(update)PUT /move/:eventId(move)DELETE /:eventId(delete)
Currently any account holding the calendar permission can edit, move, or delete any event regardless of who created it. This is acceptable while everyone holding it is a trusted admin.
Fix (updated 2026-09-06): fetch the event first and verify event.createdByUserId === res.locals.admin.id before allowing the mutation — createdById, the legacy INT, is no longer written and is gone at step 5. Rather than an isAdmin flag, the bypass belongs in the permission model that already exists: requireAppAccess('calendar', 'manage') alongside the current access role, which needs a row in APP_ROLES on both sides and nothing else.
3. Activation token has no expiry
Superseded for new accounts (2026-09-05). The admin module (
src/models/admin/) replaced account creation for the feedback, tickets and admin apps: accounts now come frominvitations, whose tokens expire after 7 days and are stored only as a SHA-256 hash. The item below still stands for the legacy calendaruserstable, which the admin module deliberately left alone - seedocs/calendar-auth-migration.md.
File: src/models/calendar/users/users.service.ts — createUser / activateUser
The email activation link is valid indefinitely. Acceptable for a small, trusted userbase.
Fix:
- Add an
activation_expirescolumn to theuserstable (e.g.DATETIME). - Set it to
NOW() + INTERVAL 24 HOURincreateUser. - Check
activation_expires > NOW()inactivateUserbefore accepting the token.
4. Password reset token has no expiry
Superseded for new accounts (2026-09-05). Password resets for admin-module accounts go through better-auth, whose reset tokens expire after one hour. As with item 3, the text below still applies to the legacy calendar
userstable.
File: src/models/calendar/users/users.service.ts — initiatePasswordReset / finalizePasswordReset
The reset token stored in pw_reset_token_hash never expires. Acceptable for a small, trusted userbase.
Fix:
- Add a
pw_reset_expirescolumn to theuserstable (e.g.DATETIME). - Set it to
NOW() + INTERVAL 15 MINUTEininitiatePasswordReset. - Check
pw_reset_expires > NOW()infinalizePasswordResetbefore accepting the token.