Skip to content

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


next -> nest_13_database - Database Integration