Asegurar APIs Node.js: Patrones que Realmente Funcionan

Asegurar APIs Node.js: Patrones que Realmente Funcionan

Limitación de tasa, validación con Zod, honeypots y cabeceras de seguridad — cada capa entre un formulario público y tu bandeja de entrada.

30 de abril de 202611 min de lecturaPor Shehzad Asadullah

Construir APIs Node.js seguras requiere más que middleware de autenticación y HTTPS. Las APIs en producción enfrentan ataques automatizados, payloads malformados y patrones de abuso que evolucionan constantemente. Esta guía cubre patrones de seguridad prácticos — rate limiting, validación de entrada, honeypots y headers Content Security Policy — que puedes implementar hoy para endurecer tu backend sin sacrificar velocidad de desarrollo ni experiencia de usuario.

Diagrama de capas de seguridad que muestra rate limiting, validación y headers protegiendo una API Node.js
La defensa en profundidad combina múltiples capas de seguridad para que ningún fallo único exponga tu API a explotación.

Estrategias de rate limiting

El rate limiting es tu primera línea de defensa contra ataques de fuerza bruta, credential stuffing y agotamiento de recursos. Sin él, un único cliente malicioso puede abrumar tu base de datos, inflar costos en la nube o denegar servicio a usuarios legítimos.

Implementa rate limiting en múltiples niveles para protección integral:

  • Rate limit global — Limita el total de solicitudes por IP en todos los endpoints para prevenir ataques volumétricos.
  • Límites específicos por endpoint — Aplica límites más estrictos a autenticación, restablecimiento de contraseña y endpoints de pago.
  • Límites basados en usuario — Tras autenticación, limita por ID de usuario para prevenir abuso a nivel de cuenta.
  • Algoritmos de ventana deslizante — Prefiere ventana deslizante sobre ventana fija para prevenir ataques burst en los límites de ventana.
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

const globalLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: "Too many requests, please try again later." },
});

const authLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: "Too many login attempts." },
});

app.use("/api/", globalLimiter);
app.use("/api/auth/login", authLimiter);

Usa Redis como backing store para contadores de rate limit en despliegues multi-instancia. Los stores en memoria se reinician cuando un servidor se reinicia y no comparten estado entre instancias balanceadas por carga, creando brechas que los atacantes pueden explotar durante despliegues rolling.

Validación y sanitización de entrada

Nunca confíes en la entrada del cliente. Cada body de solicitud, parámetro de query y valor de header es potencialmente malicioso hasta que se valide. Bibliotecas de validación basadas en esquemas como Zod o Joi proporcionan parsing type-safe que rechaza datos malformados antes de que lleguen a tu lógica de negocio o consultas a base de datos.

Validación de esquema con Zod

Define esquemas estrictos para cada endpoint y valida datos entrantes en el límite del route handler. Rechaza solicitudes que fallen la validación con respuestas 400 descriptivas en lugar de permitir que datos parciales o corruptos se propaguen.

import { z } from "zod";

const createUserSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().min(1).max(100).trim(),
  age: z.number().int().min(13).max(120).optional(),
  role: z.enum(["user", "admin"]).default("user"),
});

app.post("/api/users", async (req, res) => {
  const result = createUserSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json({
      error: "Validation failed",
      details: result.error.flatten().fieldErrors,
    });
  }

  const user = await userService.create(result.data);
  res.status(201).json(user);
});

Más allá de la validación de esquema, sanitiza entradas de cadena para prevenir inyección NoSQL, XSS en contenido almacenado y path traversal en endpoints de subida de archivos. Consultas parametrizadas y métodos ORM protegen contra inyección SQL, pero las bases de datos NoSQL requieren igual vigilancia — nunca pases objetos de usuario crudos directamente a operadores de consulta MongoDB.

Campos honeypot para detección de bots

Los honeypots son campos de formulario ocultos que los usuarios legítimos nunca ven ni completan, pero los bots automatizados suelen rellenar. Cuando un campo honeypot contiene un valor, puedes rechazar silenciosamente el envío sin revelar el mecanismo de detección al operador del bot.

Implementa honeypots en formularios públicos — formularios de contacto, páginas de registro y envíos de comentarios:

  • Agrega un input de texto con un nombre como website_url o fax_number.
  • Ocúltalo con CSS posicionado fuera de pantalla, no display: none, que los bots sofisticados detectan.
  • Agrega tabindex="-1" y autocomplete="off" para prevenir foco accidental.
  • En el servidor, rechaza cualquier envío donde el campo honeypot no esté vacío.
  • Incluye un campo timestamp y rechaza envíos completados en menos de dos segundos.
// Server-side honeypot check
app.post("/api/contact", (req, res) => {
  const { honeypot, submittedAt, ...validFields } = req.body;

  if (honeypot) {
    // Silently accept to avoid tipping off the bot
    return res.status(200).json({ success: true });
  }

  const elapsed = Date.now() - submittedAt;
  if (elapsed < 2000) {
    return res.status(200).json({ success: true });
  }

  // Process legitimate submission
  processContactForm(validFields);
  res.status(200).json({ success: true });
});

Los honeypots no reemplazan CAPTCHA ni rate limiting, pero eliminan un volumen significativo de tráfico bot no sofisticado con cero fricción para el usuario.

Headers Content Security Policy

Content Security Policy es un header de respuesta HTTP que instruye a los navegadores qué recursos pueden cargarse. Para APIs que sirven respuestas HTML — dashboards admin, sitios de documentación o páginas SSR — CSP previene ataques XSS bloqueando scripts inline y recursos externos no autorizados.

Configura CSP usando Helmet, el middleware de seguridad estándar para aplicaciones Express:

import helmet from "helmet";

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        connectSrc: ["'self'", "https://api.example.com"],
        frameSrc: ["'none'"],
        objectSrc: ["'none'"],
        upgradeInsecureRequests: [],
      },
    },
    hsts: {
      maxAge: 31536000,
      includeSubDomains: true,
      preload: true,
    },
  })
);

Incluso para APIs solo JSON, establece estos headers de seguridad complementarios:

  • Strict-Transport-Security — Fuerza conexiones HTTPS para todas las solicitudes futuras.
  • X-Content-Type-Options: nosniff — Previene ataques de MIME type sniffing.
  • X-Frame-Options: DENY — Bloquea clickjacking mediante embedding en iframe.
  • Referrer-Policy: strict-origin-when-cross-origin — Limita la filtración de información referrer.
  • Permissions-Policy — Deshabilita funciones del navegador innecesarias como cámara y geolocalización.

Patrones de autenticación y autorización

El rate limiting y la validación protegen el perímetro, pero la autenticación y autorización protegen recursos individuales. Sigue estos patrones establecidos para APIs Node.js que sirven aplicaciones frontend modernas.

  • Almacena JWTs en cookies httpOnly, secure, SameSite — nunca en localStorage donde XSS puede exfiltrarlos.
  • Implementa rotación de refresh token con detección de reutilización para limitar el radio de explosión del robo de tokens.
  • Aplica control de acceso basado en roles a nivel de ruta, no solo a nivel de UI.
  • Valida firmas y expiración JWT en cada solicitud protegida — no confíes solo en payloads decodificados.
  • Registra fallos de autenticación con direcciones IP y user agents para análisis forense.

Monitorización de seguridad y respuesta a incidentes

Los patrones de seguridad solo son efectivos cuando se combinan con observabilidad. Registra violaciones de rate limit, fallos de validación y activaciones de honeypot en una plataforma de logging centralizada. Configura alertas para patrones anómalos — un pico repentino de respuestas 401 puede indicar una campaña de credential stuffing, mientras que más errores 400 podrían señalar un ataque fuzzing buscando vulnerabilidades.

Realiza revisiones de seguridad regulares de tu superficie API. Herramientas automatizadas como OWASP ZAP y npm audit detectan vulnerabilidades conocidas, pero la revisión manual de lógica de autorización, manejo de subida de archivos y puntos de integración de terceros sigue siendo esencial. La seguridad no es una funcionalidad que envías una vez — es una práctica continua que evoluciona junto a tu aplicación y el panorama de amenazas.

¿Te gustó la lectura?

¿Tienes un proyecto o una idea en mente? Me encantaría conocerla.

Contáctame