Skip to content

fix(collegemap): read the session signing secret from the environment - #398

Open
returnsvoidjanet wants to merge 1 commit into
mainfrom
fix/collegemap-session-secret
Open

fix(collegemap): read the session signing secret from the environment#398
returnsvoidjanet wants to merge 1 commit into
mainfrom
fix/collegemap-session-secret

Conversation

@returnsvoidjanet

Copy link
Copy Markdown
Contributor

Closes a live authentication bypass on college.petalcat.dev.

The bug

auth.ts signed session cookies with a hardcoded constant:

const SESSION_SECRET = "college-map-secret-key-change-in-production";

The cookie is base64(userId + ":" + sha256(userId + SESSION_SECRET)). This repo is public, so that constant is a published signing key — a user id plus that string is a valid session for that user. And the user ids are not secret either: the anonymous /calendar page renders every one of them.

So anyone could sign in as any of the 14 accounts, with no password, from the public internet. Verified rather than inferred — the constant has no env override anywhere in the app, and the ids really are in the anonymous HTML.

The fix

Read it from $env/dynamic/private, with no default. A fallback string would silently recreate the same bug on any host that forgot to set it, and an app that refuses to boot is a much better failure than one that boots forgeable. It is narrowed through a function returning string so the type is honest rather than !-asserted.

The CI build job now writes a build-time placeholder, because SvelteKit's postbuild analyse step imports server modules — the same reason that step already writes DATABASE_URL.

Verification

Both directions, not just the happy one:

  • pnpm --filter collegemap build with the var set builds clean.
  • The same build without it fails with exactly the new message. The guard does the work; it is not decorative.
  • Full check job run locally: check, lint:impeccable, test, manypkg, typesync:check, lint:knip, lint:knip:prod, vp dedupe --check — all green.

Deploying this

The container needs SESSION_SECRET in its environment or it will not start. That is intended. Generate it on the box with openssl rand -base64 48; it never belongs in the repo.

Everyone gets logged out once, because the old cookies were signed with the old secret. Worth knowing given that nobody remembers their password — account claiming is the separate piece of work.

Not in scope here

The cookie is still secure: false, passwords are still single-round salted SHA-256, and signup still caps passwords at 4 to 8 characters. Those are real and they are next. This PR is the one that stops the bleeding.

The session cookie is base64(userId + ":" + sha256(userId + SESSION_SECRET)), and
SESSION_SECRET was a literal in this file. This repo is public, so that string was a
published signing key: anyone with a user id could mint a valid session for that user.
The user ids are not secret either - the calendar page renders every one of them to
anonymous visitors.

Read it from $env/dynamic/private instead, with no fallback value. A default would
silently recreate exactly this bug on any host that forgot to set it, and an app that
refuses to boot is a much better failure than one that boots forgeable. It is narrowed
through a function returning string so the type is honest rather than asserted.

CI needs it too: the postbuild analyse step imports the server modules, which is why the
build job already writes DATABASE_URL. A build-time placeholder is enough there.

Deploying this invalidates every existing session - the old cookies were signed with the
old secret - so everyone is logged out once.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant