Spraxium logoSpraxium

Seguridad, Guards y Middleware

Entiende el stack de hardening por defecto y cómo extenderlo de forma segura. Aplica guards o middleware globales y por ruta con un orden de ejecución predecible.

La seguridad empieza antes de los controladores

HttpServer aplica middleware de seguridad antes de invocar cualquier handler de controlador. Este cableado en tiempo de startup da a cada endpoint la misma política base desde la primera request, sin requerir decoraciones explícitas en cada ruta. El stack de seguridad corre en un orden fijo que el código de usuario no puede reordenar, asegurando que el hardening de headers y los límites de body sean siempre la primera línea de defensa.

Cuando HTTP está habilitado, el runtime conecta estas protecciones en esta secuencia exacta:

  1. Remoción opcional de headers X-Powered-By y Server (hidePoweredBy, por defecto true).
  2. SecurityHeadersMiddleware envolviendo secureHeaders de Hono (a menos que headers sea false).
  3. CorsMiddleware envolviendo cors de Hono (solo cuando security.cors está configurado).
  4. BodyLimitMiddleware envolviendo bodyLimit de Hono (a menos que bodyLimit sea false).
  5. Instancias globales de middleware desde @HttpClientModule().
  6. Guards globales desde @HttpClientModule() a través de GuardExecutor.
  7. Guards por controlador y por ruta desde @HttpGuards a través de GuardExecutor.

Después de este stack, se ejecuta el middleware de nivel ruta desde decoradores @HttpUseMiddleware y finalmente se ejecuta el propio handler. Este modelo por capas implica que las decisiones de guard suceden después del hardening base y que tus decisiones de middleware ocurren cuando los headers de seguridad ya están establecidos.

Valores de seguridad por defecto

Cada header y cada límite de seguridad tiene un valor seguro por defecto desde SECURITY_DEFAULTS. Solo necesitas sobrescribir valores cuando tu despliegue requiera otro comportamiento. Cuando se omite un subcampo, se aplica el valor por defecto. Cuando se establece en false, esa capa se desactiva por completo.

Configuración de seguridad y middleware

import { defineHttp } from '@spraxium/http';
import { AppModule } from '../src/app.module';

export const httpPlugin = defineHttp({
  enabled: true,
  host: process.env.HTTP_HOST ?? '0.0.0.0',
  port: Number(process.env.HTTP_PORT ?? 3000),
  apiKey: process.env.HTTP_API_KEY ?? '',
  sharding: false,
  security: {
    hidePoweredBy: true,
    headers: {
      frameOptions: 'DENY',
      contentSecurityPolicy: "default-src 'none'",
      referrerPolicy: 'no-referrer',
      hstsMaxAge: 31_536_000,
      hstsIncludeSubDomains: true,
    },
    cors: {
      origins: ['https://admin.example.com'],
      methods: ['GET', 'POST', 'PATCH'],
      allowedHeaders: ['Content-Type', 'Authorization', 'X-Api-Key'],
      credentials: true,
      maxAge: 600,
    },
    bodyLimit: { maxSize: 256_000 },
  },
  module: AppModule,
});

Pipeline de guards y la interfaz HttpGuard

Los guards son el límite de autenticación y autorización. Cada guard implementa la interfaz HttpGuard, que define un único método: canActivate(ctx: Context): Promise<boolean>. Los guards declarados en @HttpClientModule() se instancian mediante el sistema de inyección de dependencias en startup y se ejecutan secuencialmente a través de GuardExecutor en cada request, después del stack de middleware de seguridad. No se requiere ninguna llamada manual a new al declarar guards en el módulo.

Cuando GuardExecutor encuentra un guard que retorna false, corta la ejecución inmediatamente y define la respuesta como { ok: false, error: 'Unauthorized' } con estado 401. No se ejecutan más guards, middleware ni handlers. Este comportamiento de short-circuit implica que el orden de guards importa: coloca primero el caso de rechazo más común para evitar ejecutar verificaciones costosas en requests no autenticadas.

La respuesta 401 por rechazo de guard se inyecta directamente, no a través del handler onError. Esto significa que los fallos de guard son finales y no pueden ser capturados ni modificados por middleware posterior. La forma de respuesta coincide con el mismo envelope usado por el resto de respuestas de error, así que los clientes no necesitan manejo especial para rechazos por guard.

@HttpGuards para protección por controlador y por ruta

@HttpGuards es un decorador de clase o método que acota el alcance de guards a un controlador específico o a una ruta individual. A diferencia de los guards globales que protegen todos los endpoints, @HttpGuards apunta a un punto concreto de la superficie de routing. El decorador acepta una o más clases guard cuyas instancias se resuelven mediante el mismo sistema DI que servicios y controladores.

@HttpGuards a nivel de clase se ejecuta en todas las rutas del controlador. @HttpGuards a nivel de método se ejecuta solo en ese handler específico. Cuando existen ambos niveles, primero se ejecutan guards de clase y luego guards de método. Esta superposición te permite proteger todo un controlador con una política común y agregar validación extra por ruta sin repetir el guard compartido.

import {
  HttpController,
  HttpGet,
  HttpPost,
  HttpGuards,
  HttpParam,
  HttpBody,
} from '@spraxium/http';
import { AdminGuard } from '../guards/admin.guard';
import { SuperAdminGuard } from '../guards/super-admin.guard';

@HttpGuards(AdminGuard)
@HttpController('/admin')
export class AdminHttpController {
  @HttpGet('/stats')
  getStats() {
    return { ok: true, activeUsers: 42 };
  }

  @HttpGuards(SuperAdminGuard)
  @HttpPost('/reset')
  reset(@HttpParam('confirm') \_confirm: string) {
    return { ok: true };
  }
}

ApiKeyGuard en detalle

ApiKeyGuard es el guard integrado que trae el paquete. Lee el header x-api-key de la request y lo compara con el valor provisto al construirse. La comparación usa timingSafeEqual del módulo crypto de Node para prevenir ataques de timing que podrían filtrar información sobre longitud o prefijo de la clave.

El guard rechaza requests en tres escenarios: header faltante (retorna false de inmediato), longitud de buffer distinta (retorna false sin comparar para evitar el requisito de igual longitud de timingSafeEqual) y valores que no coinciden (retorna false tras comparación de tiempo constante). Esta estrategia de rechazo por capas evita exponer la longitud de la clave por diferencias de tiempo de respuesta.

ApiKeyGuard se activa automáticamente cuando apiKey tiene valor en defineHttp. El runtime crea y registra internamente la instancia del guard, por lo que no necesitas declararlo en @HttpClientModule(). El arreglo guards queda reservado para guards personalizados que requieren tus propias dependencias resueltas por DI.

Modelo de ejecución de middleware

El paquete soporta dos puntos de entrada de middleware con distintos alcances y mecanismos de registro.

Las instancias globales de middleware se declaran en @HttpClientModule() como un arreglo de objetos HttpMiddleware. Se registran sobre el path Hono '*', lo que significa que se ejecutan en cada request después del stack de seguridad y antes de la ejecución de guards. Usa middleware global para preocupaciones transversales que aplican sin condición, como request logging vía LoggerMiddleware.

El middleware de nivel ruta usa tokens de clase y el decorador @HttpUseMiddleware. El token de clase debe mapearse a una instancia concreta en @HttpClientModule(). RouteBuilder resuelve el token durante el binding de rutas e inserta el handler middleware antes del handler de ruta en la cadena Hono. @HttpUseMiddleware de nivel clase se ejecuta antes de @HttpUseMiddleware de nivel método, y ambos se ejecutan después de middleware global y guards.

Todo middleware implementa la interfaz HttpMiddleware: handle(ctx: Context, next: Next): Promise<undefined | Response>. Llamar await next() transfiere el control al siguiente middleware o handler. No llamar next() termina la request en ese middleware, que es cómo RateLimitMiddleware rechaza requests que superan el límite.

Referencia de middleware integrado

Estrategia de rate limiting

RateLimitMiddleware implementa un rate limiter de ventana deslizante en memoria. Cada cliente se identifica por el valor del header x-forwarded-for, o 'unknown' cuando falta ese header. El middleware mantiene un Map<string, WindowEntry> donde cada entrada registra el conteo de requests y la marca de tiempo de inicio de ventana.

Cuando llega una request, el middleware verifica si la ventana actual expiró. Si expiró, reinicia la entrada. Si la ventana sigue activa y el conteo supera max, el middleware responde { ok: false, error: 'Too many requests' } con estado 429 sin llamar next(). Un intervalo periódico de limpieza elimina entradas de clientes que no enviaron requests dentro de la duración de la ventana.

Para usar rate limiting por ruta, extiende RateLimitMiddleware con tu propia configuración y regístralo en middlewareProviders. Este patrón mantiene visible la configuración de límites en la clase middleware y evita números mágicos en argumentos de decoradores.

Confianza de proxy e identificación IP

El middleware de rate-limit lee x-forwarded-for directamente sin verificar la fuente. En producción detrás de proxies reversos o balanceadores, esto significa que debes asegurar que tu infraestructura de proxy establezca correctamente ese header. Si el header falta, todas las requests se agrupan bajo la clave 'unknown', lo que en la práctica crea un único bucket global de rate limit. Si el header puede falsificarse porque el proxy no lo sobrescribe, atacantes pueden evadir límites enviando IPs falsas en headers.

La interfaz SecurityConfig incluye un campo trustedProxies con un arreglo proxies (por defecto ['127.0.0.1', '::1']). Usa este campo para documentar tu configuración de proxies confiables y asegúrate de que tu arquitectura de despliegue solo permita a proxies confiables establecer headers de forwarding.

Checklist de hardening operativo

Comienza con defaults seguros

Habilita headers base y límites de body salvo que exista un requisito claro para relajarlos. Una postura de denegación por defecto es más fácil de auditar que una postura permisiva parchada más tarde. Sobrescribe headers individuales solo cuando tu despliegue lo requiera.

Conecta guards globales en AppHttpModule

Declara clases guard personalizadas en @HttpClientModule() para proteger todos los endpoints con la misma política. El framework resuelve dependencias de guard desde el contenedor DI, por lo que los guards pueden inyectar servicios como AppEnv sin cableado manual. Usa apiKey en defineHttp para activar ApiKeyGuard automáticamente sin declararlo en el arreglo guards.

Acota protección con @HttpGuards

Aplica @HttpGuards a nivel de controlador o método en endpoints que requieran lógica de autorización distinta o adicional. Esto mantiene el guard global enfocado en la política base y mueve verificaciones específicas al punto de declaración. Los guards de clase se ejecutan antes que los de método dentro del mismo controlador.

Agrega LoggerMiddleware para observabilidad

Registra new LoggerMiddleware() en @HttpClientModule() para obtener logging base de requests. El middleware registra método, path, estado y duración. Status codes 400 o superiores se registran en nivel warn para facilitar filtrado.

Acota rate limits a endpoints sensibles

Extiende RateLimitMiddleware con configuraciones por ruta y regístralo vía middlewareProviders. Aplícalo con @HttpUseMiddleware sobre endpoints que gestionen operaciones costosas o mutaciones sensibles en lugar de aplicar un único límite global.

Valida configuración de proxy y confianza IP

Si rate limiting o logging de auditoría dependen de IP de cliente, verifica cómo tu proxy reverso reenvía x-forwarded-for. Una configuración incorrecta de confianza puede agrupar usuarios no relacionados en un solo bucket o permitir suplantación de identidad por clientes maliciosos.

La postura de seguridad sigue siendo tu responsabilidad de diseño

El paquete entrega primitivas y defaults seguros, pero las decisiones finales de política deben ser explícitas en tu AppModule y en tu arquitectura de despliegue. Revisa headers de seguridad, orígenes CORS y configuración de guards en cada revisión de despliegue.