fix(security): add production env validation and sanitize .env.example
- Add startup env validation that fails fast in production if critical vars (JWT_SECRET, JWT_REFRESH_SECRET, DATABASE_URL, CORS_ORIGINS, REDIS_HOST) are missing - Fix CORS_ORIGINS to throw in production instead of defaulting to localhost - Replace hardcoded dev passwords in .env.example with CHANGE_ME placeholders - Add missing vars to .env.example (CORS_ORIGINS, SMTP_*, FIREBASE, LOG_LEVEL) - Warn on missing optional payment/storage vars at startup Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { LoggerService } from '@modules/shared';
|
||||
import { LoggerService, validateEnv } from '@modules/shared';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import helmet from 'helmet';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
validateEnv();
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
bufferLogs: true,
|
||||
rawBody: true,
|
||||
@@ -67,7 +68,11 @@ async function bootstrap() {
|
||||
app.use(cookieParser());
|
||||
|
||||
// ── CORS ──
|
||||
const allowedOrigins = (process.env['CORS_ORIGINS'] ?? 'http://localhost:3000')
|
||||
const corsOrigins = process.env['CORS_ORIGINS'];
|
||||
if (!corsOrigins && process.env['NODE_ENV'] === 'production') {
|
||||
throw new Error('CORS_ORIGINS must be set in production');
|
||||
}
|
||||
const allowedOrigins = (corsOrigins ?? 'http://localhost:3000')
|
||||
.split(',')
|
||||
.map((o) => o.trim());
|
||||
app.enableCors({
|
||||
|
||||
62
apps/api/src/modules/shared/infrastructure/env-validation.ts
Normal file
62
apps/api/src/modules/shared/infrastructure/env-validation.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Validates that critical environment variables are set in production.
|
||||
* Call this at application startup before any module initialization.
|
||||
*/
|
||||
|
||||
const REQUIRED_IN_PRODUCTION: readonly string[] = [
|
||||
'JWT_SECRET',
|
||||
'JWT_REFRESH_SECRET',
|
||||
'DATABASE_URL',
|
||||
'CORS_ORIGINS',
|
||||
'REDIS_HOST',
|
||||
];
|
||||
|
||||
const REQUIRED_WHEN_USED: ReadonlyMap<string, string> = new Map([
|
||||
['VNPAY_TMN_CODE', 'VNPay payments'],
|
||||
['VNPAY_HASH_SECRET', 'VNPay payments'],
|
||||
['MOMO_PARTNER_CODE', 'MoMo payments'],
|
||||
['MOMO_ACCESS_KEY', 'MoMo payments'],
|
||||
['MOMO_SECRET_KEY', 'MoMo payments'],
|
||||
['ZALOPAY_APP_ID', 'ZaloPay payments'],
|
||||
['ZALOPAY_KEY1', 'ZaloPay payments'],
|
||||
['ZALOPAY_KEY2', 'ZaloPay payments'],
|
||||
['MINIO_ACCESS_KEY', 'Media storage'],
|
||||
['MINIO_SECRET_KEY', 'Media storage'],
|
||||
]);
|
||||
|
||||
export function validateEnv(): void {
|
||||
const isProduction = process.env['NODE_ENV'] === 'production';
|
||||
|
||||
if (!isProduction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const key of REQUIRED_IN_PRODUCTION) {
|
||||
if (!process.env[key]) {
|
||||
missing.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`Missing required environment variables in production:\n ${missing.join('\n ')}\n` +
|
||||
'See .env.example for the full list of variables.',
|
||||
);
|
||||
}
|
||||
|
||||
// Warn about payment/storage vars that are empty — services will fail at
|
||||
// runtime when invoked, but we surface the warning early.
|
||||
const warnings: string[] = [];
|
||||
for (const [key, feature] of REQUIRED_WHEN_USED) {
|
||||
if (!process.env[key]) {
|
||||
warnings.push(` ${key} (${feature})`);
|
||||
}
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
console.warn(
|
||||
`[env-validation] The following optional vars are unset — related features will fail at runtime:\n${warnings.join('\n')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,3 +11,4 @@ export { maskPii } from './pii-masker';
|
||||
export { ThrottlerBehindProxyGuard } from './guards/throttler-behind-proxy.guard';
|
||||
export { FileValidationPipe } from './pipes/file-validation.pipe';
|
||||
export type { FileValidationOptions } from './pipes/file-validation.pipe';
|
||||
export { validateEnv } from './env-validation';
|
||||
|
||||
Reference in New Issue
Block a user