Skip to contentExploitQuest

Lesson 1 of 1 in Secrets Have No Fallbacks

A Default Is No Secret At All

The fallback exists so development is convenient, and it is the value running in production.

3 min read

Not yet reviewed

Somebody adds a signing secret. It needs a value locally, so it gets a fallback. The fallback is in the repository, the deploy forgets to set the real one, and the application starts perfectly.

Starts, and is broken

const SECRET = process.env.CSRF_SECRET ?? 'dev-secret-change-me'

Refuses to start

const SECRET = process.env.CSRF_SECRET
if (!SECRET) throw new Error('CSRF_SECRET is not set')

The first has no failure mode you can observe. Tokens are signed, they verify, everything works — with a key that is in a public repository or in every developer's shell history.

Why this one is worse than it looks

Most misconfiguration announces itself. A missing database URL crashes on the first query; a wrong port refuses connections. A missing secret with a fallback produces an application that behaves exactly correctly in every test you have, and is forgeable by anyone who has read the default.

Note

This is the shape worth learning, not the specific bug: a safety control whose absence looks identical to its presence. When you find one, the fix is always to make the absence loud.

Throwing on start, not on use

Check at startup rather than at the point of use. A secret validated lazily fails on the first request that needs it, which might be a week after the deploy and is certainly not while anybody is watching the logs.

Read every required secret when the process starts
Throw with the variable's name in the message
Never log the value, only the name
  1. Line 1A deploy that is going to fail should fail before it takes traffic, not on the first user who happens to hit the path.
  2. Line 2"Configuration error" costs somebody an hour. "CSRF_SECRET is not set" costs them a minute.
  3. Line 3The error will end up in a log aggregator, a ticket and possibly a screenshot. The name is enough to fix it.

How this platform enforces it

CSRF_SECRET and RATE_LIMIT_SECRET throw when unset. There is no fallback and no default, deliberately — the codebase treats a hardcoded fallback secret as identical to having no secret, because it is, and because it would ship.

Wrenlearner

That makes local development annoying, though. Every new contributor hits a wall.

Rookmentor

They hit a wall with the variable's name in it, once, and then it is in their .env forever. Weigh that against the alternative, which is that nobody hits any wall and one day the wall is in production with everybody's tokens signed by a string from a README.

Which of these is the safest way to handle a missing signing secret?

Tip

Audit for this with a grep for ?? and || next to process.env. Every hit is either a genuinely optional setting or a secret with a default, and the two are easy to tell apart once you are looking.