nest_12_security - Security Best Practices¶
Your NestJS app is a target Every endpoint , every dependency , every misconfigured CORS policy is a potential entry point The framework gives you tools - helmet for headers , throttler for rate limiting , pipes for validation - but only if you actually use them This section covers the security layers you need before you deploy anything that faces the internet
what's in here¶
- Helmet for security headers
- CORS configuration (don't use wildcards in prod)
- CSRF protection
- Rate limiting with @nestjs/throttler
- Input validation and SQL injection prevention
- Dependency scanning
- Secure defaults checklist
helmet - security headers¶
npm install helmet
// main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import helmet from 'helmet'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.use(helmet())
// sets:
// Content-Security-Policy
// X-Content-Type-Options: nosniff
// X-Frame-Options: SAMEORIGIN
// X-XSS-Protection: 0 (deprecated but still relevant)
// Strict-Transport-Security (if HTTPS)
// Referrer-Policy
// Permissions-Policy
await app.listen(3000)
}
Helmet sets ~15 HTTP security headers that browsers respect Content-Security-Policy prevents XSS by restricting what resources can load X-Content-Type-Options prevents MIME type sniffing Strict-Transport-Security enforces HTTPS
API-only apps don't need CSP as much , but the rest still matters Always install helmet - it's one line and covers the most common header-based vulnerabilities
CORS - cross-origin resource sharing¶
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
// BAD: origin: '*' - opens your API to any website
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Correlation-Id'],
credentials: true,
maxAge: 3600 // cache preflight for 1 hour
})
await app.listen(3000)
}
Never use origin: '*' in production - it allows any website to make authenticated requests from a user's browser Specify exact origins your frontend runs on: ['https://app.example.com', 'https://admin.example.com'] credentials: true is required for cookies/auth headers - but only works with explicit origins , not wildcards For development , use your localhost: ['http://localhost:4200', 'http://localhost:3000']
CSRF protection¶
npm install csurf @types/csurf
CSRF tokens protect against attacks where a malicious site makes authenticated requests using the victim's cookies
// main.ts
import * as csurf from 'csurf'
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.use(csurf({ cookie: true }))
// csurf adds a CSRF token to every response
// frontend must include it as X-CSRF-Token header on mutating requests
await app.listen(3000)
}
If your API uses cookies (session-based auth) , you need CSRF protection If your API uses Authorization headers (Bearer tokens) and never sets auth cookies , CSRF doesn't apply SPAs with JWT in localStorage don't need CSRF - but that brings XSS risks instead. Pick your poison
rate limiting with @nestjs/throttler¶
npm install @nestjs/throttler
import { Module } from '@nestjs/common'
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'
import { APP_GUARD } from '@nestjs/core'
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'short',
ttl: 1000, // 1 second
limit: 10 // 10 requests per second
},
{
name: 'medium',
ttl: 60000, // 1 minute
limit: 100 // 100 requests per minute
},
{
name: 'long',
ttl: 300000, // 5 minutes
limit: 300 // 300 requests per 5 minutes
}
])
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard // global rate limiting
}
]
})
export class AppModule {}
Override limits per route:
import { Controller, Post } from '@nestjs/common'
import { SkipThrottle, Throttle } from '@nestjs/throttler'
@Controller('auth')
export class AuthController {
@Post('login')
@Throttle({ short: { limit: 3 }, medium: { limit: 10 } })
// stricter limits on auth endpoints - prevent brute force
async login() {}
@Post('register')
@Throttle({ short: { limit: 2 } })
// even stricter on registration - prevent account creation abuse
async register() {}
@Get('health')
@SkipThrottle() // health checks should never be rate limited
async health() {}
}
Global throttling protects against DoS Stricter per-route throttling on auth endpoints prevents brute force @SkipThrottle() for health checks and monitoring endpoints
input validation and injection prevention¶
Pipes handle input validation (nest_09_pipes) , but injection prevention is an additional layer:
import { Injectable, PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common'
@Injectable()
export class NoSqlInjectionPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata): any {
if (typeof value === 'string') {
// block common NoSQL injection patterns
const patterns = [
/\$where/,
/\$ne/,
/\$gt/,
/\$regex/,
/\$exists/,
/".*\".*\{/
]
for (const pattern of patterns) {
if (pattern.test(value)) {
throw new BadRequestException('invalid input detected')
}
}
}
if (typeof value === 'object' && value !== null) {
for (const key of Object.keys(value)) {
if (key.startsWith('$') || key.includes('.')) {
throw new BadRequestException('invalid parameter name')
}
}
}
return value
}
}
This pipe blocks MongoDB injection operators ($where , $ne , $gt) Apply it globally or on specific endpoints dealing with database queries For SQL databases , use parameterized queries - NEVER concatenate user input into SQL strings
dependency scanning¶
# scan for known vulnerabilities in dependencies
npm audit
# OWASP dependency check
npx audit-ci --moderate
# Snyk (more comprehensive)
npm install -g snyk
snyk test
snyk monitor # continuous monitoring
Run npm audit in CI - fail builds on moderate+ vulnerabilities Use Snyk or Dependabot for continuous monitoring NestJS itself has a decent security track record , but your transitive dependencies (class-validator , passport , typeorm) can have CVEs
secure defaults checklist¶
Before deploy: - [ ] Helmet enabled with security headers - [ ] CORS configured with specific origins (no wildcards) - [ ] Rate limiting active (global + stricter on auth) - [ ] Validation pipe with whitelist=true , forbidNonWhitelisted=true - [ ] JWT secret is a strong random value in environment variable (not in code) - [ ] JWT tokens expire in 15 minutes or less - [ ] Refresh tokens are revokable - [ ] HTTPS enforced (Strict-Transport-Security header) - [ ] Express trust proxy configured if behind reverse proxy - [ ] Error responses don't include stack traces - [ ] npm audit passes with no high/critical vulnerabilities - [ ] Secrets never logged (see nest_10_filters)
// trust proxy - required when behind Nginx/ELB
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule)
// trust proxy headers from reverse proxy
// IMPORTANT: without this , rate limiting sees all traffic from proxy IP
app.getHttpAdapter().getInstance().set('trust proxy', 1)
await app.listen(3000)
}
Without trust proxy , your app sees all traffic coming from the proxy's IP instead of the real client IP Rate limiting won't work properly , and IP-based features (geolocation , allowlists) will be wrong
prerequisites¶
nest_11_auth - Authentication & Passport