fix(security): reject placeholder/weak JWT secrets at startup
The env-validation module previously only checked that JWT_SECRET and JWT_REFRESH_SECRET were _present_ — it accepted any value, including known placeholders like "CHANGE_ME". This meant a developer could copy .env.example verbatim and run the app with predictable, forgeable tokens. Changes: - Add FORBIDDEN_SECRET_VALUES blocklist (case-insensitive) with 23 common placeholder strings (CHANGE_ME, secret, password, test, etc.) - Enforce minimum 32-character length for JWT secrets (NIST HMAC guidance) - Export validateJwtSecret() for direct testing and reuse - Update .env.example: replace "CHANGE_ME" with generation instructions - Add 14 unit tests covering placeholder rejection, length enforcement, missing-var errors, and production-mode validation Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Validates that critical environment variables are set at application startup.
|
||||
*
|
||||
* Security-sensitive vars (JWT secrets) are required in ALL environments.
|
||||
* Infrastructure vars (DATABASE_URL, CORS, Redis) are required only in production.
|
||||
* Security-sensitive vars (JWT secrets) are required in ALL environments and must
|
||||
* not be placeholder/weak values. Infrastructure vars (DATABASE_URL, CORS, Redis)
|
||||
* are required only in production.
|
||||
*/
|
||||
|
||||
const ALWAYS_REQUIRED: readonly string[] = [
|
||||
@@ -33,6 +34,56 @@ const REQUIRED_WHEN_USED: ReadonlyMap<string, string> = new Map([
|
||||
['ZALO_APP_SECRET', 'Zalo OAuth'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Known placeholder values that must never be used as real secrets.
|
||||
* Comparison is case-insensitive to catch common variants.
|
||||
*/
|
||||
const FORBIDDEN_SECRET_VALUES: readonly string[] = [
|
||||
'change_me',
|
||||
'changeme',
|
||||
'your_jwt_secret',
|
||||
'your_jwt_secret_change_me',
|
||||
'your_jwt_refresh_secret',
|
||||
'your_jwt_refresh_secret_change_me',
|
||||
'secret',
|
||||
'jwt_secret',
|
||||
'jwt_refresh_secret',
|
||||
'supersecret',
|
||||
'super_secret',
|
||||
'mysecret',
|
||||
'my_secret',
|
||||
'password',
|
||||
'test',
|
||||
'default',
|
||||
'placeholder',
|
||||
'replace_me',
|
||||
'replaceme',
|
||||
'fixme',
|
||||
'todo',
|
||||
'xxx',
|
||||
];
|
||||
|
||||
/** Minimum length for JWT secrets — NIST SP 800-117 recommends ≥256 bits for HMAC keys. */
|
||||
const MIN_SECRET_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* Validates that a JWT secret is not a placeholder value, is long enough, and
|
||||
* does not appear to be a trivially weak key.
|
||||
*
|
||||
* @returns An error message string if the value is invalid, or `null` if OK.
|
||||
*/
|
||||
export function validateJwtSecret(key: string, value: string): string | null {
|
||||
if (FORBIDDEN_SECRET_VALUES.includes(value.toLowerCase().trim())) {
|
||||
return `${key} is set to a known placeholder value ("${value}"). Generate a real secret: \`openssl rand -base64 48\``;
|
||||
}
|
||||
|
||||
if (value.length < MIN_SECRET_LENGTH) {
|
||||
return `${key} is too short (${value.length} chars, minimum ${MIN_SECRET_LENGTH}). Generate a strong secret: \`openssl rand -base64 48\``;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateEnv(): void {
|
||||
const isProduction = process.env['NODE_ENV'] === 'production';
|
||||
const missing: string[] = [];
|
||||
@@ -52,6 +103,23 @@ export function validateEnv(): void {
|
||||
);
|
||||
}
|
||||
|
||||
// Reject placeholder / weak JWT secret values — these are dangerous even in
|
||||
// development because tokens signed with predictable keys are trivially forgeable.
|
||||
const secretErrors: string[] = [];
|
||||
for (const key of ALWAYS_REQUIRED) {
|
||||
const error = validateJwtSecret(key, process.env[key]!);
|
||||
if (error) {
|
||||
secretErrors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (secretErrors.length > 0) {
|
||||
throw new Error(
|
||||
`Insecure JWT secret configuration:\n ${secretErrors.join('\n ')}\n` +
|
||||
'Generate secure secrets with: openssl rand -base64 48',
|
||||
);
|
||||
}
|
||||
|
||||
if (!isProduction) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user