feat(security): add security hardening — Helmet, CORS, rate limiting, input sanitization

- Add Helmet with CSP, HSTS, referrer policy
- Configure CORS with environment-based origins
- Add global validation pipe with whitelist mode
- Add SanitizeInputMiddleware for XSS prevention
- Add ThrottlerBehindProxyGuard for rate limiting
- Add FileValidationPipe for upload security
- Set request body size limit to 1MB

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 02:04:13 +07:00
parent 5e44456d11
commit f3081d92fc
6 changed files with 197 additions and 1 deletions

View File

@@ -1,5 +1,7 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { LoggerService } from '@modules/shared';
import helmet from 'helmet';
import { AppModule } from './app.module';
async function bootstrap() {
@@ -7,6 +9,65 @@ async function bootstrap() {
const logger = app.get(LoggerService);
app.useLogger(logger);
// ── Security Headers (Helmet) ──
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: { policy: 'same-origin' },
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}),
);
// ── CORS ──
const allowedOrigins = (process.env['CORS_ORIGINS'] ?? 'http://localhost:3000')
.split(',')
.map((o) => o.trim());
app.enableCors({
origin: allowedOrigins,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Correlation-Id'],
exposedHeaders: ['X-Correlation-Id'],
credentials: true,
maxAge: 86400,
});
// ── Global Validation Pipe ──
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
disableErrorMessages: process.env['NODE_ENV'] === 'production',
}),
);
// ── Request Body Size Limit ──
// Express default is 100kb; explicitly set for clarity
const expressApp = app.getHttpAdapter().getInstance();
const { json, urlencoded } = await import('express');
expressApp.use(json({ limit: '1mb' }));
expressApp.use(urlencoded({ extended: true, limit: '1mb' }));
// ── Trust Proxy (for rate limiting behind reverse proxy) ──
expressApp.set('trust proxy', 1);
const port = process.env['PORT'] ?? 3001;
await app.listen(port);
logger.log(`API running on http://localhost:${port}`, 'Bootstrap');