The secret that signs your JWTs or sessions is the key to every user's identity. If it is hardcoded in source, or falls back to a default when the environment variable is missing, anyone who knows it can forge a valid token for any user, including an admin. Read it from the environment with no fallback, and fail loudly if it is not set.
Why it's a problem
Your server trusts a token because it can verify the signature with this secret. If an attacker has the secret, they can mint a token with any claims they want, a different user id, an admin flag, and your server will accept it as genuine. A fallback like process.env.JWT_SECRET || 'dev-secret' is the same as hardcoding, because the moment the variable is unset in production the default is used.
The pattern
const token = jwt.sign( payload, process.env.JWT_SECRET || "dev-secret" );
The fix
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error("JWT_SECRET not set");
const token = jwt.sign(payload, secret);Why AI tools write this
The fallback pattern is a common training-data idiom because it makes the code run in development without setup. It works locally, so it survives to production, where a missing environment variable silently activates the public default value.
The quick fix
- Read the signing secret from an environment variable with no fallback default.
- Throw or refuse to start if the secret is not set, rather than using a default.
- Use a long, random secret, and rotate it if it was ever hardcoded (this invalidates existing sessions).
Common questions
What happens if my JWT signing secret leaks?
Anyone who has it can sign tokens with any claims, including fake user ids or admin flags, and your server will accept them as genuine. Rotate the secret immediately, which invalidates all existing sessions.
Is a fallback like process.env.JWT_SECRET || 'dev' a real problem?
Yes, every time the environment variable is unset, the fallback becomes the real secret. In a misconfigured deployment or a test environment that shares code, the literal default is used instead.
How long should a JWT secret be?
At least 256 bits (32 bytes) of cryptographically random data. Generate it with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" and store it in your secret manager.