Step 4 of docs/calendar-auth-migration.md, and the close of
DEFERRED_SECURITY.md item 1: no calendar route reads sessionId/sessionKey from
the query string any more, so a live credential no longer travels through
access logs, browser history and Referer headers.
The four write routes sit behind requireAppAccess('calendar'), which also
narrows who may edit from "any activated @nachklang.art account" to an
explicit per-user permission. They answer 401 signed out and 403 without the
permission, where they previously answered 403 for both.
The three read routes cannot use the middleware: one URL serves an anonymous
visitor, an iCal subscription holding a shared password, and a signed-in
editor who should see drafts. They resolve the session optionally instead, and
a signed-in user without the calendar permission is treated as anonymous
rather than refused - so they keep the public calendar access anyone has.
That public calendar staying anonymous is load-bearing: nachklang.art reads it
to show the next upcoming event. It is now pinned at both the password-table
and the route level, and so is the rule that a shared password can never be
used to write.
credentials.service.ts loses its session half and becomes the password table
it always wanted to be. The shared passwords survive only for iCal clients,
which cannot send a cookie.
Writes record the author as an admin user id and no longer have a legacy int
to write, which is what migration 003 makes room for.
/calendar/users/* is left in place: nothing calls it and a session it mints
opens nothing, but they are still live password-accepting endpoints, so
removing them belongs with the rest of the legacy path in step 5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.