🚨 ¡Nueva review! ¡Mi teclado ideal! ⌨️ Perfecto para programar, el Logitech MX Keys S . ¡Échale un ojo! 👀

Blindando tu API en NestJS: Helmet, CORS, Rate Limiting y más

Serie NestJS #12 — Helmet, CORS, @nestjs/throttler, CSRF, compresión y protección contra ataques comunes

Escrito por domin el 6 de abril de 2026

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.

Muro de fuego protegiendo una API NestJS contra ataques externos con Helmet, CORS y rate limiting.

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
🧱 Capa perimetral

Headers HTTP seguros, CORS, rate limiting, compresión. Filtra ataques ANTES de llegar a tu lógica.

🪪 Autenticación

¿QUIÉN eres? JWT + Passport + refresh tokens.

🛡️ Autorización

¿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

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

Instalación de Helmet 0 / 1
$
Pulsa para ejecutar el siguiente comando

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

HeaderQué haceProtege contra
Content-Security-PolicyControla qué recursos puede cargar el navegadorXSS, inyección de scripts
X-Content-Type-OptionsEvita que el navegador adivine el MIME typeMIME sniffing
Strict-Transport-SecurityFuerza HTTPS en futuras visitasDowngrade attacks, cookie hijacking
X-Frame-OptionsImpide que la página se cargue en un iframeClickjacking
X-DNS-Prefetch-ControlControla la resolución DNS anticipadaDNS rebinding
X-Download-OptionsEvita que IE ejecute descargas en el contexto del sitioDrive-by downloads
X-Permitted-Cross-Domain-PoliciesRestringe políticas cross-domain de AdobeExfiltración de datos vía Flash/PDF
Referrer-PolicyControla qué información de referencia se envíaFiltració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

Comprobando headers de Helmet 0 / 1
$
Pulsa para ejecutar el siguiente comando

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

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(),
Úsalo cuando...
  • 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
Evítalo cuando...
  • 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. pone un tope.

4.1. Instalación

Instalación de @nestjs/throttler 0 / 1
$
Pulsa para ejecutar el siguiente comando

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:

ThrottlerTTLLímiteProtege contra
short1 segundo3 requestsRáfagas instantáneas, scripts automatizados
medium10 segundos20 requestsUso agresivo sostenido
long60 segundos100 requestsAbuso 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

Instalación de compression 0 / 1
$
Pulsa para ejecutar el siguiente comando

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

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 AuthorizationNoNo. El header no se envía automáticamente
Token en cookie httpOnlySí. Las cookies se envían automáticamente
Sesiones con cookiesSí. 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:

MedidaHerramientaPost
Validación de inputsclass-validator + ValidationPipePost 6
Queries parametrizadasTypeORM Repository + QueryBuilderPost 8
Passwords hasheadosbcrypt (10 salt rounds)Post 10
Autenticación JWTPassport + JwtStrategy + refresh tokensPost 10
Autorización RBACRolesGuard + @Roles()Post 11
Headers HTTP segurosHelmetPost 12 (este)
CORS configuradoapp.enableCors() con orígenes explícitosPost 12 (este)
Rate limiting@nestjs/throttler + ThrottlerGuardPost 12 (este)
Payload size limitexpress json({ limit })Post 12 (este)
Compresióncompression middlewarePost 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:

⛑️ Helmet

Headers HTTP de seguridad automáticos. Una línea, múltiples ataques cubiertos: XSS, clickjacking, MIME sniffing, downgrade.

🌐 CORS

Lista explícita de orígenes permitidos. Configuración dinámica con variables de entorno. Protección del navegador.

⏱️ Rate Limiting

Tres ventanas de throttling (short/medium/long). Por usuario autenticado o por IP. Overrides por endpoint.

📦 Payload Limits

Body limitado a 10KB. Protege contra ataques de consumo de memoria. Primero en el pipeline.

🗜️ Compresión

Respuestas comprimidas con gzip. Menos ancho de banda, respuestas más rápidas. Solo para payloads > 1KB.

🔗 Pipeline completo

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?