Vamos allá con el post número 12 de la serie NestJS y el último del bloque de Seguridad. En el post 10 montamos autenticación con Passport y JWT. En el post 11 añadimos autorización con Guards, roles y RBAC. Ya sabemos quién es el usuario y qué puede hacer y ahora toca la tercera capa que es proteger a la API del mundo exterior.
Da igual que tu auth sea perfecta si un atacante puede tumbar tu servidor con 10.000 requests por segundo, inyectar scripts maliciosos vía headers o explotar una mala configuración de CORS.
Toda la base que traemos: Docker (post 1), controllers (post 2), DI (post 3), módulos (post 4), middleware y pipeline (post 5), validación (post 6), PostgreSQL con TypeORM (posts 7-9), autenticación (post 10) y autorización (post 11).
EA, amo a blindarno.

1. Las tres capas de seguridad
Antes de meternos en código, visualicemos las capas de seguridad que hemos ido construyendo:
Internet → [Helmet + CORS + Rate Limiting] → [Auth: JWT] → [AuthZ: Roles/RBAC] → Handler
Headers HTTP seguros, CORS, rate limiting, compresión. Filtra ataques ANTES de llegar a tu lógica.
¿QUIÉN eres? JWT + Passport + refresh tokens.
¿QUÉ puedes hacer? Guards, @Roles(), RBAC, CASL.
La capa perimetral es la primera línea de defensa que funciona antes de la autenticación. Aunque alguien tenga un JWT válido, si está haciendo 1.000 requests por segundo, el rate limiter lo frena. Si intenta inyectar un script vía headers, Helmet lo bloquea.
2. Helmet: headers HTTP seguros
💡 Helmet Middleware de Express/NestJS que establece headers HTTP de seguridad automáticamente. Configura Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security y otros headers que protegen contra XSS, clickjacking, MIME sniffing y más ataques. Más info → es un middleware que configura headers HTTP de seguridad. Con una línea de código puedes cubrir múltiples vectores de ataque.
2.1. Instalación
2.2. Configuración en main.ts
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { type NestExpressApplication } from '@nestjs/platform-express';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Helmet: headers de seguridad
app.use(helmet());
// ValidationPipe global (del post 6)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.listen(3000);
}
bootstrap();
Una línea app.use(helmet()) e ya. Ahora vemos que añade esto:
2.3. Headers que establece Helmet
| Header | Qué hace | Protege contra |
|---|---|---|
Content-Security-Policy | Controla qué recursos puede cargar el navegador | XSS, inyección de scripts |
X-Content-Type-Options | Evita que el navegador adivine el MIME type | MIME sniffing |
Strict-Transport-Security | Fuerza HTTPS en futuras visitas | Downgrade attacks, cookie hijacking |
X-Frame-Options | Impide que la página se cargue en un iframe | Clickjacking |
X-DNS-Prefetch-Control | Controla la resolución DNS anticipada | DNS rebinding |
X-Download-Options | Evita que IE ejecute descargas en el contexto del sitio | Drive-by downloads |
X-Permitted-Cross-Domain-Policies | Restringe políticas cross-domain de Adobe | Exfiltración de datos vía Flash/PDF |
Referrer-Policy | Controla qué información de referencia se envía | Filtración de URLs privadas |
2.4. Configuración personalizada
app.use(
helmet({
// Content-Security-Policy personalizado
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
},
},
// HSTS: forzar HTTPS durante 1 año
hsts: {
maxAge: 31536000, // 1 año en segundos
includeSubDomains: true,
preload: true,
},
// Permitir iframes si necesitas embeddings
// frameguard: false,
}),
);
Para APIs REST puras: si tu API solo devuelve JSON y no sirve HTML, puedes simplificar el CSP. Pero es mejor dejarlo restrictivo por defecto. Si algún día tu API devuelve HTML (por ejemplo, un panel de Swagger), los headers ya están ahí protegiéndote.
2.5. Verificando los headers
Sin Helmet esos headers no existen y tu API está expuesta a todos los ataques de la tabla. Con una línea, los tienes todos.
3. CORS: Cross-Origin Resource Sharing
💡 CORS Cross-Origin Resource Sharing. Mecanismo del navegador que controla qué dominios pueden hacer peticiones a tu API. Si tu frontend está en app.example.com y tu API en api.example.com, el navegador bloquea las peticiones a menos que la API incluya los headers CORS correctos. Más info → es un mecanismo de seguridad del navegador (no del servidor). Controla qué orígenes pueden hacer peticiones a tu API.
3.1. ¿Cómo funciona CORS?
1. Frontend en https://app.example.com hace fetch a https://api.example.com/users
2. Navegador: "Eh, el origen es diferente. Voy a preguntar al servidor"
3. Navegador envía OPTIONS request (preflight) con:
- Origin: https://app.example.com
- Access-Control-Request-Method: GET
- Access-Control-Request-Headers: Authorization
4. Servidor responde con:
- Access-Control-Allow-Origin: https://app.example.com ← "Sí, te dejo"
- Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
- Access-Control-Allow-Headers: Authorization, Content-Type
5. Navegador: "OK, el servidor lo permite" → ejecuta la petición real
Si el servidor no responde con los headers correctos, el navegador bloquea la petición y nunca llega a tu código.
Importante: CORS es protección del navegador. Una petición desde
curl, Postman o un servidor backend ignora CORS completamente. CORS protege a los usuarios de tu frontend contra peticiones maliciosas desde otros sitios.
3.2. Configuración en NestJS
// src/main.ts
async function bootstrap(): Promise<void> {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.use(helmet());
// CORS configurado
app.enableCors({
// Orígenes permitidos
origin: [
'https://app.example.com',
'https://admin.example.com',
],
// Métodos HTTP permitidos
methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
// Headers que el cliente puede enviar
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
],
// Headers que el cliente puede leer de la respuesta
exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
// Permitir cookies/credenciales
credentials: true,
// Cache del preflight (en segundos)
maxAge: 3600,
});
await app.listen(3000);
}
3.3. CORS dinámico con ConfigService
En producción, los orígenes permitidos vienen de variables de entorno:
// src/main.ts
import { ConfigService } from '@nestjs/config';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const configService = app.get(ConfigService);
app.use(helmet());
const allowedOrigins = configService
.getOrThrow<string>('CORS_ORIGINS')
.split(',')
.map((origin) => origin.trim());
app.enableCors({
origin: (
origin: string | undefined,
callback: (error: Error | null, allow?: boolean) => void,
) => {
// Permitir requests sin origin (curl, Postman, server-to-server)
if (!origin) {
callback(null, true);
return;
}
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origen no permitido por CORS: ${origin}`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
maxAge: 3600,
});
await app.listen(3000);
}
Y en el .env:
# .env
CORS_ORIGINS=https://app.example.com,https://admin.example.com
En desarrollo:
# .env.development
CORS_ORIGINS=http://localhost:3001,http://localhost:5173
Valídalo con Joi como hicimos en el post 7:
// src/config/env.validation.ts
CORS_ORIGINS: Joi.string().required(),
- Lista explícita de orígenes permitidos. Cada dominio que necesite acceso, en la lista
- Variables de entorno para los orígenes. Diferentes por entorno (dev, staging, prod)
- credentials: true si tu frontend envía cookies o headers de autenticación
- origin: "*" con credentials: true. El navegador lo bloquea directamente, no funciona
- origin: true o origin: "*" en producción. Cualquier web puede hacer requests a tu API
- Desactivar CORS completamente. Tu frontend no podrá hacer peticiones desde el navegador
4. Rate Limiting con @nestjs/throttler
De nada sirve tener auth perfecta si alguien puede hacer fuerza bruta al login con 10.000 intentos por segundo. 💡 Rate Limiting Mecanismo que limita el número de peticiones que un cliente puede hacer en un período de tiempo. Protege contra ataques de fuerza bruta, DDoS a nivel de aplicación y abuso de la API. Se implementa por IP, por usuario, por endpoint o combinaciones. Más info → pone un tope.
4.1. Instalación
4.2. Configuración global
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
throttlers: [
{
name: 'short',
ttl: configService.get<number>('THROTTLE_SHORT_TTL', 1000),
limit: configService.get<number>('THROTTLE_SHORT_LIMIT', 3),
},
{
name: 'medium',
ttl: configService.get<number>('THROTTLE_MEDIUM_TTL', 10000),
limit: configService.get<number>('THROTTLE_MEDIUM_LIMIT', 20),
},
{
name: 'long',
ttl: configService.get<number>('THROTTLE_LONG_TTL', 60000),
limit: configService.get<number>('THROTTLE_LONG_LIMIT', 100),
},
],
}),
}),
// ... otros módulos
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
4.3. ¿Qué son los throttlers múltiples?
Cada throttler define una ventana de tiempo diferente. Todos deben pasar para que la request se permita:
| Throttler | TTL | Límite | Protege contra |
|---|---|---|---|
short | 1 segundo | 3 requests | Ráfagas instantáneas, scripts automatizados |
medium | 10 segundos | 20 requests | Uso agresivo sostenido |
long | 60 segundos | 100 requests | Abuso prolongado, scraping |
Un usuario normal nunca llega a estos límites, pero un bot se los peta fácil.
4.4. Variables de entorno
# .env
THROTTLE_SHORT_TTL=1000
THROTTLE_SHORT_LIMIT=3
THROTTLE_MEDIUM_TTL=10000
THROTTLE_MEDIUM_LIMIT=20
THROTTLE_LONG_TTL=60000
THROTTLE_LONG_LIMIT=100
4.5. Override por endpoint con @Throttle()
Algunos endpoints necesitan límites diferentes. El login debería ser más restrictivo que un listado:
import { Throttle, SkipThrottle } from '@nestjs/throttler';
@Controller('auth')
export class AuthController {
// Login: mucho más restrictivo (fuerza bruta)
@Post('login')
@Throttle({
short: { ttl: 1000, limit: 1 }, // 1 intento por segundo
medium: { ttl: 60000, limit: 5 }, // 5 intentos por minuto
long: { ttl: 3600000, limit: 20 }, // 20 intentos por hora
})
login(@Body() loginDto: LoginDto): Promise<AuthTokens> {
return this.authService.login(loginDto);
}
// Registro: restrictivo también
@Post('register')
@Throttle({
short: { ttl: 1000, limit: 1 },
medium: { ttl: 60000, limit: 3 },
long: { ttl: 3600000, limit: 10 },
})
register(@Body() registerDto: RegisterDto): Promise<AuthTokens> {
return this.authService.register(registerDto);
}
}
@Controller('health')
export class HealthController {
// Health check: sin rate limiting (los load balancers lo llaman constantemente)
@Get()
@SkipThrottle()
check(): { status: string } {
return { status: 'ok' };
}
}
4.6. Skip a nivel de controller
// Todo el controller sin rate limiting
@SkipThrottle()
@Controller('webhooks')
export class WebhooksController {
// Los webhooks externos no deberían tener rate limit
// (verificas con firma, no con rate limiting)
}
4.7. ThrottlerGuard custom: limitando por usuario autenticado
Por defecto, ThrottlerGuard limita por IP. Pero detrás de un proxy o un NAT, muchos usuarios comparten la misma IP. Mejor limitar por usuario cuando esté autenticado:
// src/common/guards/custom-throttler.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
import { type JwtPayload } from '../../auth/interfaces/jwt-payload.interface';
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
protected getTracker(req: Record<string, unknown>): Promise<string> {
const user = req.user as JwtPayload | undefined;
// Si el usuario está autenticado, limitar por user ID
// Si no (rutas @Public), limitar por IP
return Promise.resolve(user?.sub ?? (req.ip as string));
}
}
Y reemplaza ThrottlerGuard en el registro global:
// src/app.module.ts
import { CustomThrottlerGuard } from './common/guards/custom-throttler.guard';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: CustomThrottlerGuard, // En vez de ThrottlerGuard
},
],
})
export class AppModule {}
4.8. Respuesta cuando se excede el límite
Cuando un cliente excede el rate limit, NestJS responde con:
{
"statusCode": 429,
"message": "ThrottlerException: Too Many Requests"
}
El header Retry-After indica cuántos segundos debe esperar el cliente.
5. Compresión de respuestas
Comprimir las respuestas reduce el ancho de banda y mejora los tiempos de respuesta, especialmente para payloads JSON grandes.
5.1. Instalación
5.2. Configuración
// src/main.ts
import compression from 'compression';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.use(helmet());
app.enableCors({ /* ... */ });
app.use(compression({
threshold: 1024, // Solo comprimir respuestas > 1KB
level: 6, // Balance entre velocidad y compresión (1-9)
}));
await app.listen(3000);
}
En producción con nginx/proxy: si usas un reverse proxy como nginx delante de tu API, es mejor que la compresión la haga nginx (más eficiente, escrito en C). Activa la compresión en NestJS solo si tu app sirve directamente al cliente sin proxy.
6. Protección contra ataques comunes
6.1. Payload Size Limit
Un atacante puede enviar un body de 100MB para consumir la memoria de tu servidor, por lo que una buena práctica sería limitar el tamaño del payload:
// src/main.ts
import { json, urlencoded } from 'express';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Limitar el tamaño del body
app.use(json({ limit: '10kb' })); // JSON bodies max 10KB
app.use(urlencoded({ extended: true, limit: '10kb' })); // Form data max 10KB
app.use(helmet());
app.enableCors({ /* ... */ });
await app.listen(3000);
}
¿Por qué 10kb? Una petición típica de crear/editar un recurso no debería superar unos pocos KB. Si tienes endpoints de file upload, esos se manejan aparte con Multer (lo veremos en un post futuro) y tienen su propio límite.
6.2. Parameter Pollution
Un atacante envía parámetros duplicados para confundir a tu lógica:
GET /users?role=admin&role=user
Express parsea esto como role: ['admin', 'user'] en vez de un string. Si tu código espera un string, puede fallar de formas inesperadas. La solución es poner el ValidationPipe con whitelist: true que configuramos en el post 6 ya se encarga de esto. Si el DTO declara role: string, class-transformer toma el último valor y el array se descarta.
6.3. NoSQL Injection (si usas MongoDB)
Esto aplica principalmente a MongoDB. Con TypeORM y PostgreSQL (nuestro caso) estamos protegidos porque TypeORM usa queries parametrizadas. Pero si algún día usas MongoDB:
// ❌ VULNERABLE: el atacante envía { "email": { "$ne": "" } }
const user = await collection.findOne({ email: req.body.email });
// ✅ PROTEGIDO: forzar string con el DTO
@IsEmail()
readonly email: string; // class-validator garantiza que es un string, no un objeto
Los DTOs con class-validator son tu primera defensa contra inyección porque valida todo lo que entra.
6.4. SQL Injection
Con TypeORM y queries parametrizadas, estamos protegidos contra SQL injection estándar:
// ✅ SEGURO: TypeORM parametriza automáticamente
const user = await this.usersRepository.findOne({
where: { email },
});
// ✅ SEGURO: Query builder también parametriza
const users = await this.usersRepository
.createQueryBuilder('user')
.where('user.email = :email', { email })
.getMany();
// ❌ PELIGROSO: query raw sin parametrizar
const users = await this.usersRepository
.query(`SELECT * FROM users WHERE email = '${email}'`);
OJITO: nunca concatenes variables directamente en strings SQL. Siempre usa parámetros (
:param) o los métodos del Repository que parametrizan automáticamente.
7. CSRF: Cross-Site Request Forgery
💡 CSRF Cross-Site Request Forgery. Ataque donde un sitio malicioso hace que el navegador del usuario envíe una petición a tu API usando las cookies de sesión del usuario. El servidor cree que es una petición legítima porque las cookies viajan automáticamente. Se previene con tokens CSRF o verificando el header Origin. Más info → es un ataque donde un sitio malicioso hace peticiones a tu API usando las cookies del usuario.
¿Necesitas protección CSRF?
Depende de cómo envías el token de autenticación:
| Método de auth | ¿Vulnerable a CSRF? | ¿Necesitas protección? |
|---|---|---|
JWT en header Authorization | No | No. El header no se envía automáticamente |
Token en cookie httpOnly | Sí | Sí. Las cookies se envían automáticamente |
| Sesiones con cookies | Sí | Sí. Mismo motivo que arriba |
Nuestra API usa JWT en el header Authorization: Bearer <token> (como configuramos en el post 10). Los headers no se envían automáticamente como las cookies. El atacante no puede incluir el header Authorization en una petición cross-origin. Por lo tanto, no somos vulnerables a CSRF con nuestra implementación actual.
Si usas cookies para el refresh token
Si decides guardar el refresh token en una cookie httpOnly (que es una práctica recomendada), necesitas protección CSRF para la ruta de refresh:
// src/main.ts
import * as cookieParser from 'cookie-parser';
app.use(cookieParser());
// src/auth/auth.controller.ts
@Post('refresh')
@HttpCode(HttpStatus.OK)
refresh(@Req() req: Request): Promise<AuthTokens> {
const refreshToken = req.cookies['refresh_token'] as string;
const origin = req.headers.origin;
// Verificar que el Origin coincide con nuestro frontend
const allowedOrigins = this.configService
.getOrThrow<string>('CORS_ORIGINS')
.split(',');
if (!origin || !allowedOrigins.includes(origin)) {
throw new ForbiddenException('Origin no válido');
}
return this.authService.refreshTokens(req.user.sub, refreshToken);
}
La verificación del header Origin es una protección CSRF simple y efectiva. El navegador siempre envía el header Origin en peticiones cross-origin y no puede ser falsificado por JavaScript.
8. El main.ts completo blindado
Juntando todo lo que hemos visto, el main.ts de producción queda así:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { type NestExpressApplication } from '@nestjs/platform-express';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import helmet from 'helmet';
import compression from 'compression';
import { json, urlencoded } from 'express';
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const configService = app.get(ConfigService);
const logger = new Logger('Bootstrap');
// 1. Payload size limits (ANTES de todo)
app.use(json({ limit: '10kb' }));
app.use(urlencoded({ extended: true, limit: '10kb' }));
// 2. Helmet: headers de seguridad
app.use(helmet());
// 3. CORS
const allowedOrigins = configService
.getOrThrow<string>('CORS_ORIGINS')
.split(',')
.map((origin) => origin.trim());
app.enableCors({
origin: allowedOrigins,
credentials: true,
methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
maxAge: 3600,
});
// 4. Compresión
app.use(compression({ threshold: 1024 }));
// 5. ValidationPipe global
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
// 6. Prefijo global (opcional)
app.setGlobalPrefix('api', {
exclude: ['health'],
});
const port = configService.get<number>('PORT', 3000);
await app.listen(port);
logger.log(`API corriendo en puerto ${port}`);
}
bootstrap();
Y el AppModule con los Guards globales:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerModule } from '@nestjs/throttler';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
import { RolesGuard } from './auth/guards/roles.guard';
import { CustomThrottlerGuard } from './common/guards/custom-throttler.guard';
import { envValidationSchema } from './config/env.validation';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: envValidationSchema,
}),
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
throttlers: [
{
name: 'short',
ttl: configService.get<number>('THROTTLE_SHORT_TTL', 1000),
limit: configService.get<number>('THROTTLE_SHORT_LIMIT', 3),
},
{
name: 'medium',
ttl: configService.get<number>('THROTTLE_MEDIUM_TTL', 10000),
limit: configService.get<number>('THROTTLE_MEDIUM_LIMIT', 20),
},
{
name: 'long',
ttl: configService.get<number>('THROTTLE_LONG_TTL', 60000),
limit: configService.get<number>('THROTTLE_LONG_LIMIT', 100),
},
],
}),
}),
// ... otros módulos
],
providers: [
// Orden: Throttler → Auth → Roles
{
provide: APP_GUARD,
useClass: CustomThrottlerGuard, // 1. ¿Estás haciendo demasiadas requests?
},
{
provide: APP_GUARD,
useClass: JwtAuthGuard, // 2. ¿Quién eres?
},
{
provide: APP_GUARD,
useClass: RolesGuard, // 3. ¿Qué puedes hacer?
},
],
})
export class AppModule {}
Mira el orden de los Guards globales:
Request → CustomThrottlerGuard → JwtAuthGuard → RolesGuard → Handler
¿Muy rápido? ¿Quién? ¿Permiso?
El throttler va primero, así, si alguien está haciendo brute force, lo frenas antes de gastar CPU verificando JWTs.
9. Checklist de seguridad
Todo lo que debería tener tu API NestJS de producción, recopilado en un solo sitio:
| Medida | Herramienta | Post |
|---|---|---|
| Validación de inputs | class-validator + ValidationPipe | Post 6 |
| Queries parametrizadas | TypeORM Repository + QueryBuilder | Post 8 |
| Passwords hasheados | bcrypt (10 salt rounds) | Post 10 |
| Autenticación JWT | Passport + JwtStrategy + refresh tokens | Post 10 |
| Autorización RBAC | RolesGuard + @Roles() | Post 11 |
| Headers HTTP seguros | Helmet | Post 12 (este) |
| CORS configurado | app.enableCors() con orígenes explícitos | Post 12 (este) |
| Rate limiting | @nestjs/throttler + ThrottlerGuard | Post 12 (este) |
| Payload size limit | express json({ limit }) | Post 12 (este) |
| Compresión | compression middleware | Post 12 (este) |
10. Errores comunes
Error 1: origin: ”*” con credentials: true
// ❌ El navegador RECHAZA esto. No funciona
app.enableCors({
origin: '*',
credentials: true,
});
Si credentials: true, el origin debe ser un dominio explícito, no el wildcard. El navegador lo bloquea por especificación.
Error 2: Helmet bloqueando tu frontend
// ❌ CSP demasiado restrictivo bloquea tu SPA
app.use(helmet());
// Tu frontend no puede cargar scripts ni estilos
// ✅ Ajusta el CSP para tu caso
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Si tu frontend lo necesita
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
}));
Si tu NestJS solo sirve una API JSON (sin HTML), el CSP por defecto de Helmet está bien. Si sirve también un panel de administración, ajústalo.
Error 3: Rate limiting sin excluir health checks
// ❌ Tu load balancer hace health checks cada segundo y se bloquea
@Controller('health')
export class HealthController {
@Get()
check(): { status: string } {
return { status: 'ok' };
}
}
// ✅ Excluir health checks del throttling
@Controller('health')
export class HealthController {
@Get()
@SkipThrottle()
check(): { status: string } {
return { status: 'ok' };
}
}
Error 4: No poner payload limit ANTES de Helmet
// ❌ El orden importa: sin limit, un body de 100MB llega a Helmet
app.use(helmet());
app.use(json({ limit: '10kb' })); // Demasiado tarde
// ✅ Payload limit primero
app.use(json({ limit: '10kb' }));
app.use(helmet());
El limit del body parser debe ir antes que cualquier otro middleware para que rechace payloads enormes antes de procesarlos.
11. Variables de entorno completas del bloque de seguridad
Después de los posts 10, 11 y 12, estas son todas las variables de entorno de seguridad:
# .env — Bloque de seguridad completo
# JWT (Post 10)
JWT_ACCESS_SECRET=tu-secreto-super-largo-para-access-tokens
JWT_REFRESH_SECRET=otro-secreto-diferente-para-refresh-tokens
JWT_ACCESS_EXPIRATION=15m
JWT_REFRESH_EXPIRATION=7d
# CORS (Post 12)
CORS_ORIGINS=https://app.example.com,https://admin.example.com
# Throttling (Post 12)
THROTTLE_SHORT_TTL=1000
THROTTLE_SHORT_LIMIT=3
THROTTLE_MEDIUM_TTL=10000
THROTTLE_MEDIUM_LIMIT=20
THROTTLE_LONG_TTL=60000
THROTTLE_LONG_LIMIT=100
Y la validación de Joi actualizada:
// src/config/env.validation.ts
import * as Joi from 'joi';
export const envValidationSchema = Joi.object({
// DB (Post 7)
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_NAME: Joi.string().required(),
// JWT (Post 10)
JWT_ACCESS_SECRET: Joi.string().required(),
JWT_REFRESH_SECRET: Joi.string().required(),
JWT_ACCESS_EXPIRATION: Joi.string().default('15m'),
JWT_REFRESH_EXPIRATION: Joi.string().default('7d'),
// CORS (Post 12)
CORS_ORIGINS: Joi.string().required(),
// Throttling (Post 12)
THROTTLE_SHORT_TTL: Joi.number().default(1000),
THROTTLE_SHORT_LIMIT: Joi.number().default(3),
THROTTLE_MEDIUM_TTL: Joi.number().default(10000),
THROTTLE_MEDIUM_LIMIT: Joi.number().default(20),
THROTTLE_LONG_TTL: Joi.number().default(60000),
THROTTLE_LONG_LIMIT: Joi.number().default(100),
// App
PORT: Joi.number().default(3000),
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
});
12. Resumen del bloque de seguridad
Con los posts 10, 11 y 12, nuestra API tiene seguridad de producción completa:
Headers HTTP de seguridad automáticos. Una línea, múltiples ataques cubiertos: XSS, clickjacking, MIME sniffing, downgrade.
Lista explícita de orígenes permitidos. Configuración dinámica con variables de entorno. Protección del navegador.
Tres ventanas de throttling (short/medium/long). Por usuario autenticado o por IP. Overrides por endpoint.
Body limitado a 10KB. Protege contra ataques de consumo de memoria. Primero en el pipeline.
Respuestas comprimidas con gzip. Menos ancho de banda, respuestas más rápidas. Solo para payloads > 1KB.
Throttler → JwtAuth → Roles → Handler. Cada capa filtra antes de que la siguiente gaste recursos.
Con esto cerramos el Bloque 3: Seguridad. Tenemos autenticación (JWT + Passport), autorización (Guards + RBAC + CASL) y protección perimetral (Helmet + CORS + Throttler). El siguiente bloque empieza con Exception Filters donde veremos cómo manejar errores de forma centralizada y profesional.
EA, nos vemos en los bares!! 🍺
Pon a prueba lo aprendido
1. ¿Cuál es la diferencia principal entre CORS y Helmet?
2. ¿Por qué no se puede usar origin: '*' con credentials: true en CORS?
3. ¿Qué ventaja tienen los throttlers múltiples (short, medium, long)?
4. ¿Por qué el CustomThrottlerGuard debería ir PRIMERO en el orden de APP_GUARD?
5. ¿Cuándo necesitas protección CSRF en tu API NestJS?