Skip to content

JWT_SECRET falls back to a public default value in production #186

@Ridanshi

Description

@Ridanshi

The backend currently registers @fastify/jwt with a hardcoded fallback secret:

secret: process.env.JWT_SECRET || 'dev-secret-change-me'

If JWT_SECRET is absent from the production environment, the application silently falls back to the publicly known default value 'dev-secret-change-me'.

Because this value is visible in the repository source, any attacker can forge valid JWTs for arbitrary users and gain authenticated access to protected API routes.


Vulnerable Code

File: apps/backend/src/app.ts

await app.register(jwt, {
  secret: process.env.JWT_SECRET || 'dev-secret-change-me',
});

Root Cause

The || fallback silently succeeds when JWT_SECRET is undefined or empty.

There is currently no startup validation ensuring that required authentication secrets are configured before the server begins accepting requests.

As a result:

  • the application boots normally,
  • JWT signing and verification continue using the public fallback secret,
  • authentication operates in an insecure state without any visible warning.

Security Impact

This creates a complete authentication bypass vulnerability.

An attacker can:

  1. read the publicly exposed fallback secret from the repository,
  2. generate arbitrary JWTs,
  3. impersonate any user by crafting tokens with arbitrary payloads,
  4. access protected API routes as authenticated users.

Example attack flow:

const jwt = require('jsonwebtoken');

const token = jwt.sign(
  {
    id: 'target-user-id',
    username: 'victim'
  },
  'dev-secret-change-me',
  { expiresIn: '30d' }
);

console.log(token);

The forged token can then be supplied through:

  • Authorization: Bearer <token>
  • authentication cookies

The backend will successfully verify the signature and execute requests as the impersonated user.

This affects all routes protected by app.authenticate.


Steps to Reproduce

  1. Start the backend without defining JWT_SECRET.
  2. Generate a JWT using the known fallback secret.
  3. Send the forged token to an authenticated endpoint such as:
GET /api/auth/me
  1. Observe that the backend accepts the forged token and returns authenticated user data.

Expected Behavior

The server must fail fast if JWT_SECRET is missing or insecure in a production environment.

The application should:

  • refuse to start,
  • log a clear actionable error,
  • never register JWT authentication with a public fallback secret.

Actual Behavior

The server starts successfully and silently uses the hardcoded development secret for JWT signing and verification.


Proposed Fix

Add startup validation before JWT plugin registration.

Suggested approach:

const REQUIRED_SECRETS = ['JWT_SECRET', 'ENCRYPTION_KEY'] as const;

for (const key of REQUIRED_SECRETS) {
  if (!process.env[key]) {
    console.error(
      `FATAL: Environment variable "${key}" is not set. Refusing to start.`
    );
    process.exit(1);
  }
}

if (
  process.env.NODE_ENV === 'production' &&
  process.env.JWT_SECRET === 'dev-secret-change-me'
) {
  console.error(
    'FATAL: JWT_SECRET is using the default development value.'
  );

  process.exit(1);
}

Then remove the insecure fallback:

// Before
secret: process.env.JWT_SECRET || 'dev-secret-change-me',

// After
secret: process.env.JWT_SECRET!,

Acceptance Criteria

  • Server exits with a non-zero code when JWT_SECRET is missing.
  • Server exits when JWT_SECRET === 'dev-secret-change-me' in production.
  • Hardcoded fallback secret is removed from JWT registration.
  • ENCRYPTION_KEY receives the same startup validation.
  • Existing authentication flows continue working with valid configuration.
  • No API contract or schema changes are introduced.

Testing Requirements

  • Validation fails when JWT_SECRET is undefined.
  • Validation fails when JWT_SECRET equals the development default in production.
  • Validation passes with a valid configured secret.
  • Existing auth integration tests continue passing.
  • JWT signing and verification behavior remain unchanged with valid configuration.

Additional Notes

This is a fail-fast security hardening fix with minimal implementation complexity and low regression risk.

The issue primarily affects deployments where environment configuration is incomplete or misconfigured (Docker, Railway, Render, CI/CD environments, etc.).

No dependency changes are required.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions