#6831·hedgedoc

Session store drops the CSRF secret when it updates an existing session

Author: lisboarochaCreated Sep 12, 2026Updated Sep 12, 2026

KnexSessionStore.set() writes a session with insert … onConflict(id).merge([...]), and csrfToken is not in the merge list (backend/src/sessions/knex-session-store.ts:120-132 on develop at f199a28; the list arrived with 8f4c1ae17). The CSRF secret is therefore only written when the session row is first inserted. A token generated for a session that already exists is never persisted, and the next state-changing request to the private API fails with 403 Missing csrf secret.

Reproduction

Measured on develop at f199a28, backend built from source, sqlite, one OIDC provider configured:

GET    /api/private/auth/oidc/<id>      302   saves the session (pendingUser), no CSRF secret
GET    /api/private/csrf/token          200   generates a secret for that existing session
DELETE /api/private/auth/pending-user   403   {"statusCode":403,"message":"Missing csrf secret"}
       (with the token from the previous step in the csrf-token header)

The first request only needs to reach the redirect; no identity provider round trip is involved. The same 403 appears on PUT /api/private/auth/pending-user at the end of a complete first OIDC login, when the token is requested after the login started. In the database the affected session rows have csrf_token set to NULL.

When it bites

Sessions use saveUninitialized: false, so a row exists only once something modifies the session. If a token is requested first, the row is inserted with its secret and later updates leave it alone, which is why most flows work. The failure needs a session that was saved before the first token request, and GET /api/private/auth/oidc/:id is one path that does that.

What I have not measured: a real browser. The frontend requests the token lazily (frontend/src/redux/csrf-token/methods.ts) and logs in or registers a guest on load (components/application-loader/initializers/login-or-register-guest.ts), which normally creates the session row with a secret before any OIDC login starts. So I would expect browsers with guest access enabled not to see this, and I cannot tell you whether a configuration without guest access does.

Fix

Add FieldNameSession.csrfToken to the merge list. Pull request to follow.