RSTR-JWT-003 — hardcoded JWT secret in source

Summary

A JWT signing call uses a literal string as the HMAC secret. As soon as the source or compiled binary leaks (open-sourcing the repo, leaking the artefact, a backup hitting public storage), the secret leaks too — and anyone with the secret can mint valid tokens for any user.

Severity

High.

Languages

JavaScript, TypeScript, Python.

What rastray flags

jwt.sign (Node) / jwt.encode (PyJWT) calls where the second positional argument is a string literal:

const token = jwt.sign({ sub: user.id }, 'my-super-secret');   // ← flagged
token = jwt.encode({'sub': user.id}, 'my-super-secret',
                   algorithm='HS256')                          # ← flagged

What rastray deliberately does not flag

  • Secrets read from process.env / os.environ.
  • Keys loaded from disk or a secret manager.
  • Public/private key arguments (asymmetric tokens) — those are long-lived material and the literal embedding is expected.

How to fix it

Load the secret from the environment or a secret manager:

const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET unset');

const token = jwt.sign({ sub: user.id }, secret, {
  algorithm: 'HS256',
  expiresIn: '15m',
});
import os
SECRET = os.environ['JWT_SECRET']

token = jwt.encode({'sub': user.id}, SECRET, algorithm='HS256')

For higher-stakes systems, switch to asymmetric signatures (RS256, EdDSA) and keep the private key in a dedicated key-management service (AWS KMS, HashiCorp Vault, GCP KMS). Verifiers only need the public key.

How to suppress

For unit tests that need a deterministic secret, suppress per-line:

// rastray-ignore: RSTR-JWT-003 — unit-test fixture secret, not production
const token = jwt.sign({ sub: 'u1' }, 'test-secret');

References