Protección de APIs en Express: autenticación, validación, límites de tasa y monitoreo.
Una guía práctica paso a paso sobre la seguridad de APIs en Express: autenticación con Passport y JWT, modelos de autorización, cifrado AES-GCM, validación, límites de tasa y registro de eventos.
Cada API que expone es una puerta hacia su sistema, y los atacantes exploran esas puertas de manera mucho más sistemática de lo que la mayoría de los equipos las prueban. La seguridad añadida después del lanzamiento tiende a dejar brechas: una ruta sin protección, una consulta construida a partir de datos brutos, un punto de acceso de inicio de sesión que acepta felizmente un millón de intentos. Esta guía explica los estándares que merecen conocerse, las amenazas que aparecen con mayor frecuencia y seis capas concretas de defensa implementadas en Node.js con Express, para que pueda auditar una API existente o crear una nueva con protección desde el primer día.
Considere lo que sigue como una lista de verificación a la que debe volver durante todo el ciclo de vida del desarrollo: antes de un lanzamiento, después de aplicar un parche y cada vez que cambie una dependencia o una ruta. Realizar estas verificaciones de forma rutinaria es la manera de detectar vulnerabilidades mientras aún son pequeñas.
Por qué las APIs merecen una atención especial en materia de seguridad
Los productos modernos se fabrican cada vez más a partir de APIs. En lugar de desarrollar todas las funcionalidades internamente, los equipos integran servicios de pago, identidad, mensajería y datos a través de interfaces bien definidas, y muchas empresas ahora lanzan productos basados en APIs. Un estudio de mercado estima que la economía de las API alcanzará aproximadamente 20 mil millones de dólares para 2026 (resumen del informe); sea cual sea la cifra exacta, la dependencia es real y está en aumento.
Esa dependencia afecta a ambos lados. Una API brinda a los clientes legítimos funcionalidades listas para usar y reutilizables, pero también proporciona a los atacantes un punto de entrada documentado y adecuado para las máquinas. Las encuestas del sector vinculan consistentemente una gran parte de las brechas de seguridad con las APIs; el informe de 2023 de Traceable atribuye el 74% de las brechas de datos a ellas.
Las APIs suelen encontrarse directamente frente a los datos más sensibles que posee una empresa: plataformas de identidad con detalles personales, registros financieros y flujos de trabajo internos. El acceso no autorizado puede causar datos dañados, servicios mal utilizados, pérdidas financieras, la pérdida de confianza de los clientes y sanciones regulatorias según la ley de protección de datos. Dado que las consecuencias son tan graves, la seguridad debe incluirse en los acuerdos de nivel de servicio junto con el tiempo de actividad, y es responsabilidad de cada equipo de producto, no solo de un grupo dedicado a la seguridad. El punto de partida lógico son los estándares ya acordados por la industria.
Estándares y marcos que vale la pena conocer
Los estándares de seguridad de API son especificaciones formales, protocolos y directrices aplicados a lo largo del ciclo de vida del desarrollo de software para que la protección sea consistente y no improvisada. Los más comunes son:
- OWASP API Security Top 10: una lista ordenada de los riesgos más críticos para las API, mantenida por el Open Web Application Security Project. Aborda problemas como la autorización a nivel de objeto defectuosa, el fraude en solicitudes del lado del servidor y el consumo ilimitado de recursos, y constituye el mejor punto de partida para una revisión de amenazas.
- OAuth 2.0 y 2.1: un marco de autorización delegada. Un cliente obtiene un token de acceso con alcances definidos y lo utiliza para actuar en nombre de un usuario sin tener que manejar nunca su contraseña; los tokens de actualización permiten al cliente obtener nuevos tokens de acceso sin tener que solicitarlo nuevamente al usuario.
- OpenID Connect (OIDC): una capa de identidad sobre OAuth. Estandariza un token de identificación y la forma en que los clientes lo validan, lo que hace posible el inicio de sesión único y la recuperación de perfiles desde proveedores de identidad, garantizando su interoperabilidad.
Estos elementos le brindan una base sólida, aunque no constituyen una lista exhaustiva.
Las amenazas contra las que se protege
Las vulnerabilidades que aparecen con mayor frecuencia en las APIs reales son:
- Autenticación defectuosa: las verificaciones de identidad débiles o inexistentes, junto con un manejo deficiente de las sesiones, permiten que un atacante robe cookies o tokens y los reutilice para acceder a sus servicios.
- Autorización a nivel de objeto defectuosa (BOLA): la API verifica que el usuario esté conectado, pero no que tenga permiso para acceder a un registro específico; por lo tanto, cambiar un ID en la URL expone los datos de otra persona o los flujos de trabajo internos.
- Inyección SQL: los datos introducidos por el atacante se concatenan en una consulta que es ejecutada por la base de datos. Casi todos los clientes de bases de datos ofrecen un mecanismo de parámetros que permite pasar valores de forma segura.
- Inyección de comandos: los datos no confiables provenientes de una solicitud llegan a la shell del sistema o al procesador de comandos, lo que permite al atacante ejecutar comandos arbitrarios con los privilegios del proceso del servidor.
Cada uno de estos casos tiene una práctica de programación correspondiente. El resto de esta guía los aborda en seis capas, utilizando JavaScript y Express en todo momento.
1. Autenticación: demostrar quién es el solicitante
Toda API protegida debe hacer que el cliente demuestre su identidad antes de realizar cualquier acción significativa, ya sea a través de un nombre de usuario y contraseña, una clave API o un token firmado. Los métodos de autenticación generalmente se dividen en cinco categorías: nombre de usuario y contraseña, autenticación multifactor, autenticación basada en tokens, autenticación basada en certificados y biometría.
Una fuente común de confusión es lo que realmente reemplaza un JWT. Las sesiones tradicionales del servidor almacenaban el estado en el servidor y dependían de las cookies del navegador para transportar un ID de sesión. Un JWT elimina la necesidad de realizar búsquedas en el servidor con cada solicitud, pero no verifica las credenciales por sí mismo: aún es necesario verificar la contraseña una vez antes de emitir el token. Por eso funciona bien un flujo híbrido. El usuario inicia sesión con correo electrónico y contraseña, el servidor emite un JWT en caso de éxito, y cada solicitud posterior solo presenta el token. La verificación de credenciales y la autenticación por solicitud se convierten en tareas separadas, y la contraseña ya no se transmite con cada llamada.
En Express, la biblioteca Passport admite ambas opciones a través de estrategias conectables. La configuración consta de tres pasos.
Paso 1: registrar una estrategia local y una estrategia JWT
La estrategia local se ejecuta una vez, al iniciar sesión, y es la encargada de buscar al usuario por correo electrónico y comparar la contraseña enviada con el hash almacenado. La estrategia JWT se ejecuta en cada solicitud protegida: extrae el token del encabezado Authorization: Bearer, verifica la firma contra JWT_SECRET y resuelve al usuario al que se hace referencia en el payload. Exportar un middleware authenticateJWT preconfigurado con session: false deja explícita la intención de no mantener estado.
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const { Strategy: JwtStrategy, ExtractJwt } = require("passport-jwt");
// Local Strategy: Verify username and password during login.
passport.use(
new LocalStrategy(
{ usernameField: "email", passwordField: "password" },
async (email, password, done) => {
// Find the user and compare the hashed password.
// If valid, return the user.
}
)
);
// JWT Strategy: Verify the token on protected requests.
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
},
async (payload, done) => {
// Find the user referenced in the token.
}
)
);
// Middleware
const authenticateJWT = passport.authenticate("jwt", { session: false, });
module.exports = { passport, authenticateJWT, };
Los callbacks de verificación se dejan como comentarios aquí, y es allí donde reside el verdadero trabajo de seguridad. Utilice un algoritmo de hash lento y con sal, como bcrypt o Argon2, para la comparación de contraseñas, y devuelva siempre el mismo mensaje genérico de error, ya sea que el correo electrónico sea desconocido o la contraseña esté incorrecta, de modo que el endpoint no pueda usarse para descubrir qué cuentas existen.
Paso 2: autenticarse al iniciar sesión y firmar un token
El manejador de inicio de sesión invoca la estrategia local a través de una función de callback personalizada. Un error es gestionado por el sistema de manejo de errores de Express; la ausencia de usuario genera un 401, mientras que una coincidencia exitosa produce un token firmado con el ID y rol del usuario, además de una fecha de vencimiento tomada de JWT_EXPIRES_IN, que por defecto es de dos horas.
const jwt = require("jsonwebtoken");
const passport = require("passport");
const login = (req, res, next) => {
passport.authenticate("local", { session: false }, (err, user, info) => {
if (err) return next(err);
if (!user) return res.status(401).json({ message: info.message, });
// Issue a signed JWT after successful authentication.
const token = jwt.sign(
{ id: user.id, role: user.role,},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || "2h",}
);
return res.status(200).json({ message: "Login successful.", token, user,});
})(req, res, next);
};
Hay dos detalles que merecen atención. El corto plazo de validez determina cuánto tiempo permanece útil un token robado; si las sesiones necesitan durar más, combine tokens de acceso cortos con un flujo de actualización como el descrito en nuestra estrategia de tokens de actualización para sistemas de autenticación en Node.js. Además, la respuesta devuelve user tal como está. Si ese objeto es una fila bruta de la base de datos, puede incluir el hash de la contraseña y campos internos, lo cual representa exactamente esa exposición excesiva de datos mencionada anteriormente. En su lugar, devuelva un subconjunto explícito como el ID, el correo electrónico y el rol.
Paso 3: proteger las rutas protegidas
Con el middleware exportado, proteger una ruta implica colocar authenticateJWT antes del controlador en la definición de la ruta. Las solicitudes sin un token válido son rechazadas antes de que se ejecute cualquier lógica de negocio.
const { Router } = require("express");
const authRouter = Router();
// Get auth middleware and sample prorected controller
const authController = require("../controllers/auth.controller");
const { authenticateJWT } = require("../middleware/authentication");
// Use JWT as a guard to protect certain routes
authRouter.get("/me", authenticateJWT, authController.me);
authRouter.patch("/password", authenticateJWT, authController.updatePassword);
Si prefiere routers más ligeros, la misma protección puede incluirse en la cadena de middleware del propio controlador. De cualquier manera, haga que la protección sea el valor por defecto para los routers y exima deliberadamente a las rutas públicas, en lugar de recordar agregar la protección una ruta por vez.
2. Autorización: decidir qué puede hacer el solicitante
La autenticación responde a “¿quién es usted?”; la autorización responde a “¿qué se le permite hacer?”. Por lo general, se ejecuta justo después de la autenticación y evalúa cada identidad contra las reglas de acceso antes de conceder o denegar una solicitud. Sin ella, cualquier usuario conectado puede leer datos sensibles o activar acciones con privilegios, y es precisamente así como surgen las vulnerabilidades BOLA.
Tres modelos cubren la mayoría de las necesidades:
- Control de acceso basado en roles (RBAC) asigna permisos a los roles y estos a los usuarios. Una API para blogs podría tener roles de administrador, editor y espectador. Es adecuado para funciones y grupos laborales estables, por lo que es común en aplicaciones empresariales.
- Control de acceso basado en atributos (ABAC) evalúa los atributos del usuario, el recurso y el entorno de la solicitud (departamento, sensibilidad del recurso, hora del día, red) en relación con las políticas establecidas. Se utiliza en APIs cuyas decisiones son altamente contextuales o cambian con frecuencia.
- Control de acceso basado en relaciones (ReBAC) otorga acceso según la relación entre un usuario y un recurso específico, como la propiedad o la pertenencia a un grupo, lo que generalmente se verifica mediante el análisis de un grafo de relaciones. Es ideal para productos colaborativos como la compartición de documentos o las plataformas sociales.
Crear la autorización por tu cuenta es una excelente forma de comprender sus matices, pero los sistemas de producción suelen delegar la emisión y validación de tokens a un proveedor de identidades. Cuando una API se registra en un proveedor como Microsoft Entra ID y se configura para aceptar tokens de tipo bearer, los endpoints sensibles validan los alcances y roles de cada token antes de ejecutarse. Un token inválido o la falta de permisos provoca un 401 Unauthorized. En Express, la ruta protegida se ve así:
app.get(
"/api/orders",
passport.authenticate("oauth-bearer", { session: false }),
(req, res) => {
res.json({ message: "Protected resource." });
}
);
Tenga en cuenta que un token validado solo establece permisos generales. Las verificaciones a nivel de objeto, como “¿Este pedido pertenece a este usuario?”, aún deben realizarse en su manejador o capa de datos, ya que ningún proveedor de identidades sabe quién es el propietario de la fila 4812 en su base de datos.
En cuanto a los protocolos en sí, confíe en estándares de la industria como OAuth 2.0, OpenID Connect y SAML. Puede implementar estos flujos por su cuenta o delegarlos a proveedores de identidad como Ping Identity, Okta, Microsoft Entra ID, AWS o IBM Security Verify.
3. Cifrado: protección de los datos en tránsito y en reposo
El cifrado convierte los datos legibles en texto cifrado que resulta inútil sin la clave adecuada. TLS protege los datos mientras se transmiten; el cifrado en reposo los protege donde se almacenan, incluyendo la base de datos. Los sistemas sensibles suelen utilizar ambos métodos, ya que sin cifrado, datos como las credenciales financieras pueden ser interceptados o extraídos de un almacenamiento comprometido.
Los enfoques principales equilibran la velocidad con la gestión de claves:
- Cifrado simétrico utiliza una clave compartida. Es muy rápido y maneja grandes volúmenes con eficacia, lo que lo convierte en la opción adecuada para el cifrado de datos inactivos y de cargas útiles, pero ambas partes deben conservar el mismo secreto de forma segura.
- Cifrado asimétrico emplea un par de claves, una pública y otra privada, por lo que no es necesario intercambiar un secreto compartido. Es considerablemente más lento y solo resulta práctico para pequeños volúmenes de datos.
- Cifrado híbrido combina ambos métodos: la criptografía asimétrica protege una clave simétrica, y esta última protege los datos en su totalidad. Se obtienen las ventajas del intercambio de claves del primer método junto con la velocidad del segundo.
Para el cifrado simétrico, el módulo crypto incorporado en Node soporta AES-256-GCM. El procesador que se muestra a continuación serializa el cuerpo de la solicitud, genera un vector de inicialización de 12 bytes nuevo, cifra los datos y devuelve el IV, la etiqueta de autenticación GCM y el texto cifrado como cadenas hexadecimales.
const crypto = require("crypto");
const algorithm = "aes-256-gcm";
const key = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
app.post("/api/orders", (req, res) => {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(algorithm, key, iv);
const encrypted = Buffer.concat([
cipher.update(JSON.stringify(req.body), "utf8"),
cipher.final(),
]);
const payload = {
iv: iv.toString("hex"),
tag: cipher.getAuthTag().toString("hex"),
data: encrypted.toString("hex"),
};
// Store or transmit the encrypted payload
res.json(payload);
});
Varias cosas hacen que esto sea correcto. La clave debe tener exactamente 32 bytes (64 caracteres hexadecimales en ENCRYPTION_KEY) y debe provenir de un gestor de secretos y no del código fuente. El IV debe ser único para cada cifrado con la misma clave; reutilizar un IV con GCM tiene consecuencias catastróficas, por lo que se genera para cada solicitud. La etiqueta de autenticación es lo que permite a la parte que descifra detectar manipulaciones, por lo que debe almacenarse junto con el texto cifrado y verificarse durante el proceso de descifrado. En un servicio real, este payload se guardaría o se reenviaría en lugar de enviárselo de vuelta al llamante como lo hace la demostración.
También está disponible el cifrado asimétrico en el mismo módulo. Los datos cifrados con una clave pública solo pueden descifrarse con la clave privada correspondiente:
const crypto = require("crypto");
const encrypted = crypto.publicEncrypt(
publicKey,
Buffer.from("Sensitive API data")
);
Dado que RSA solo puede cifrar un payload más pequeño que el tamaño de su clave, publicEncrypt es adecuado para valores cortos como un campo secreto o una clave simétrica, pero no para documentos completos. Esa limitación es la que aborda el flujo de trabajo híbrido: generar una clave AES temporal, cifrar el payload con ella, cifrar la clave AES con la clave pública RSA del destinatario y enviar ambas. El destinatario utiliza su clave privada para recuperar la clave AES y luego descifra el payload. La mayoría de las APIs nunca necesitarán esto en el código de la aplicación, ya que TLS ya realiza un intercambio similar, pero merece la pena entenderlo para escenarios de cifrado punto a punto.
4. Validación y saneamiento de entradas
Una vez que su API acepta los datos del cliente, no puede predecir qué llegará. Los cuerpos de solicitud mal formados, los fragmentos SQL y las cargas de script parecen cadenas ordinarias hasta que algo los interpreta. Dos técnicas complementarias abordan este problema. La validación rechaza las entradas que violan sus reglas estructurales y semánticas. La saneamiento transforma las entradas aceptadas en una forma segura y normalizada antes de que lleguen a sus procesadores.
Imponga primero el tipo de contenido
La verificación más sencilla es el formato de la solicitud en sí. Esta pequeña fábrica de middleware utiliza req.is() para confirmar el Content-Type y responde con 415 Unsupported Media Type en caso contrario. Puede instalarse de forma global, por router o por endpoint.
const requireContentType = (type) => (req, res, next) => {
if (!req.is(type)) {
return res.status(415).json({ error: "Unsupported Media Type", });
}
next();
};
app.post("/api/users", requireContentType("application/json"),
(req, res) => {
res.json({ message: "User created." });
}
);
Valide la estructura y el significado del cuerpo
Una vez aplicado el formato, la capa siguiente verifica que la solicitud esté bien formada. Utilizando express-validator, se guardan las reglas en un módulo de validador dedicado. Este requiere una dirección de correo sintácticamente válida, ejecuta una verificación personalizada asíncrona que rechaza las direcciones ya existentes en la base de datos y exige una longitud mínima de contraseña de ocho caracteres.
const { body } = require("express-validator");
const { getUserEmail } = require("../db/queries");
const validateRegistration = [
body("email")
.isEmail()
.withMessage("Invalid email format")
.custom(async (value) => {
if (await getUserEmail(value)) {
throw new Error("Email is already in use");
}
return true;
}),
body("password")
.isLength({ min: 8 })
.withMessage("Password must be at least 8 characters long"),
];
module.exports = { validateRegistration }
Luego, el array de validadores se incluye en la cadena de middleware de la ruta. Dentro del manejador, validationResult(req) recopila todos los fallos, y la ruta devuelve un código 400 con la lista completa en lugar de continuar.
const { validationResult } = require("express-validator");
const { validateRegistration } = require("../validators/userValidator");
app.post("/api/register", validateRegistration, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.json({ message: "Registration successful." });
});
Sanitizar después de la validación
Dado que Express procesa los middleware en orden, una cadena de sanitización puede colocarse justo después de la validación. Aquí, el nombre se recorta y se escapa en HTML, y la dirección de correo se normaliza.
const sanitizeRegistration = [
body("firstName").trim().escape(),
body("email").normalizeEmail(),
];
app.post(
"/api/register",
validateRegistration,
sanitizeRegistration,
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.json({ message: "Registration successful." });
}
);
Aquí el orden es importante de una manera sutil. La verificación de unicidad en el validador se ejecuta antes que normalizeEmail(), por lo que dos formas diferentes de escribir la misma dirección podrían pasar desapercibidas y generar cuentas duplicadas. Normalizar los datos antes de realizar la búsqueda, o garantizar la unicidad en el valor normalizado a nivel de base de datos, cierra esa brecha. También sea cuidadoso con escape(): el codificado HTML de los datos de entrada protege a las plantillas que muestran esos valores, pero modifica los datos almacenados; muchas equipos prefieren almacenar valores en bruto y codificarlos al momento de la salida. Si está considerando bibliotecas de validación, nuestra comparación entre Zod y express-validator aborda los pros y contras.
Use consultas parametrizadas para la base de datos
Nunca construya SQL concatenando la entrada del usuario. Los clientes de base de datos como pg y los ORM como Prisma admiten consultas parametrizadas, que envían el texto de la consulta y los valores por separado para que la base de datos siempre trate la entrada como datos y nunca como SQL ejecutable.
Con pg, cree un pool de conexiones una vez y guárdelo para sus módulos de datos:
const { Pool } = require("pg");
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
module.exports = pool;
Las consultas luego utilizan marcadores de posición numerados ($1, $2) con los valores proporcionados como un array separado. Incluso si email contiene una comilla seguida de DROP TABLE, se almacena como una cadena literal.
app.post("/api/users", async (req, res) => {
const { email, name } = req.body;
await pool.query(
"INSERT INTO users (email, name) VALUES ($1, $2)",
[email, name]
);
res.status(201).json({ message: "User created." });
});
Juntos, estos elementos le brindan una pipeline en capas. Express-validator le permite aislar reglas como unidades reutilizables que se ejecutan como middleware y reportan cada fallo, mientras que la parametrización significa que incluso la entrada que escapa de la validación no puede reescribir sus consultas.
5. Limitación y control de tasas
La limitación de tasas establece cuántas solicitudes puede realizar un cliente dentro de un período determinado. Esto reduce los intentos de fuerza bruta y denegación de servicio, además de evitar que un usuario intensivo agote los recursos de todos los demás.
Se pueden aplicar limitaciones en diferentes dimensiones:
- Por cliente: las solicitudes se cuentan según cada clave API o dirección IP. Cuando un cliente alcanza el límite, debe esperar a que se reinicie el período o solicitar una cuota mayor, generalmente en un plan de pago.
- Por geografía o tiempo: las limitaciones varían según la región o el período de tiempo; por ejemplo, se permite más tráfico desde las regiones donde operan los clientes y se endurecen las restricciones en aquellos lugares de donde proviene tráfico sospechoso.
Existen muchos algoritmos (ventana fija, ventana deslizante, cubo de tokens), y no es necesario implementarlos uno mismo para comenzar. El middleware express-rate-limit cuenta las solicitudes por dirección IP de forma predeterminada. El ejemplo a continuación establece un presupuesto general de 100 solicitudes cada 15 minutos para todo lo que se encuentra bajo /api, así como un límite mucho más estricto de cinco intentos cada cinco minutos para el inicio de sesión, con un mensaje personalizado para las solicitudes rechazadas.
const rateLimit = require("express-rate-limit");
// Apply to all API routes
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
});
// Apply stricter limits to authentication endpoints
const loginLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 5,
message: "Too many login attempts. Please try again later.",
});
app.use("/api", apiLimiter);
app.post("/api/login", loginLimiter, (req, res) => {
res.json({ message: "Login successful." });
});
Las APIs públicas suelen asignar una clave a cada consumidor, y limitar por esa clave es más justo que hacerlo por IP, ya que muchos usuarios pueden compartir una misma dirección detrás de un proxy corporativo. Un keyGenerator personalizado lee el encabezado X-API-Key y lo utiliza como identificador del contador.
const rateLimit = require("express-rate-limit");
const apiKeyLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 1000,
keyGenerator: (req) => req.get("X-API-Key"),
});
app.use("/api", apiKeyLimiter);
Tal como está escrito, cada solicitud que omite el encabezado genera la misma clave undefined y comparte un único bucket. En la práctica, se deben rechazar las solicitudes sin clave más temprano en el proceso o recurrir a la dirección IP.
Ralentización de endpoints costosos
El control de velocidad determina la rapidez con la que se aceptan las solicitudes para evitar que picos repentinos sobrecarguen el servicio. La siguiente configuración utiliza el mismo middleware con intervalos muy cortos: como máximo diez solicitudes por segundo a través de la API, y solo una solicitud de búsqueda cada dos segundos por cliente, ya que la búsqueda es el endpoint que consume más recursos.
const rateLimit = require("express-rate-limit");
// Throttle all API requests
const apiThrottle = rateLimit({
windowMs: 1000, // 1 second
max: 10, // Allow up to 10 requests per second
});
// Apply a stricter throttle to resource-intensive endpoints
const searchThrottle = rateLimit({
windowMs: 2000, // 2 seconds
max: 1, // Allow 1 request every 2 seconds
message: "Please wait before sending another search request.",
});
app.use("/api", apiThrottle);
app.get("/api/search", searchThrottle, (req, res) => {
res.json({ results: [] });
});
Estrictamente hablando, esto sigue siendo un límite de frecuencia con intervalos cortos: las solicitudes excesivas son rechazadas con un 429, no se retardan. Si desea un verdadero control de velocidad que ralenticé a los clientes antes de rechazarlos, un paquete complementario como express-slow-down añade retrasos progresivos. Tenga también en cuenta que el almacén en memoria por defecto cuenta las solicitudes por proceso, por lo que detrás de un balanceador de carga con varias instancias necesita un almacén compartido como Redis para que los límites se mantengan. Las versiones recientes de express-rate-limit también denominan la opción max como limit; consulte la documentación de la versión que instale. Para una alternativa ligera en TypeScript, consulte nuestro artículo sobre un limitador de frecuencia mínimo para Express.
6. Registro, monitoreo y detección de incidentes
No se puede responder a un ataque que nunca se ve. El registro guarda las solicitudes y respuestas junto con sus metadatos, contexto, tiempos de ejecución y códigos de error, para que pueda solucionar problemas, realizar auditorías y comprender el uso real. El monitoreo observa la actividad en tiempo real, sigue indicadores como la latencia, las tasas de error y el ancho de banda, y detecta anomalías que podrían indicar abuso, incumplimiento de los objetivos de nivel de servicio o una vulnerabilidad en uso.
Los principales enfoques, con sus ventajas e inconvenientes:
- Registro de solicitudes con middleware como Morgan captura cada solicitud HTTP entrante de forma económica, pero no proporciona información sobre el estado del sistema.
Cualquiera de estos se conecta a Express. A continuación se presentan cuatro componentes comunes.
Registro de solicitudes con Morgan
Al registrar a Morgan en el formato combined, se escribe una línea al estilo Apache para cada solicitud, que incluye el método, la ruta, el estado, el tamaño de la respuesta y el agente del usuario.
const express = require("express");
const morgan = require("morgan");
const app = express();
// Log every incoming request
app.use(morgan("combined"));
app.get("/api/users", (req, res) => {
res.json({ message: "Users retrieved successfully." });
});
Registro estructurado de aplicaciones con Winston
Winston registra los eventos como objetos estructurados. Al registrar los IDs de usuario y de pedido al crear un pedido, se genera un registro de auditoría que se puede buscar posteriormente.
const winston = require("winston");
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
],
});
app.post("/api/orders", (req, res) => {
logger.info("Order created", {
userId: req.user.id,
orderId: req.body.id,
});
res.status(201).json({ message: "Order created." });
});
Tenga cuidado con lo que se incluye en los registros. Los IDs de usuario están bien; las contraseñas, tokens, números completos de tarjetas y los cuerpos completos de las solicitudes no lo están, y los registros son un lugar común donde pueden filtrarse datos sensibles.
Registro centralizado de errores
Un middleware de manejo de errores de Express, reconocible por sus cuatro argumentos, captura los errores de cualquier ruta. Este registra el mensaje junto con la ruta y el método, y devuelve un código genérico 500 para que las trazas de llamada y los detalles internos nunca lleguen al cliente.
app.use((err, req, res, next) => {
/* Logger is built as an independent module or class */
logger.error(err.message, {
path: req.originalUrl,
method: req.method,
});
res.status(500).json({
error: "Internal Server Error",
});
});
Exposición de métricas para Prometheus
La biblioteca prom-client recopila las métricas predeterminadas del proceso de Node.js y las expone en un endpoint /metrics para que Prometheus las extraiga.
const client = require("prom-client");
client.collectDefaultMetrics();
app.get("/metrics", async (req, res) => {
res.set("Content-Type", client.register.contentType);
res.end(await client.register.metrics());
});
Ese endpoint revela detalles internos sobre su servicio, por lo que debe restringirse a su red de monitoreo o protegerse con autenticación en lugar de dejarlo público.
Los frameworks con enfoque definido son útiles aquí. NestJS incluye un registrador integrado y una estructura que facilita la implementación de funciones de registro y métricas para cada endpoint, mientras que con un framework sin enfoque definido como Express hay que añadir el registro de forma deliberada, generalmente como middleware que se ejecuta antes de enviar la respuesta. La detección de incidentes se basa entonces en estos registros: alertas para rutas con patrones sospechosos, como un aumento en las respuestas 401 o rechazos por límite de frecuencia, dirigidas al canal de notificaciones que realmente utiliza su equipo.
La seguridad como proceso continuo
Ningún artículo abarca todo, pero antes de cualquier lanzamiento puede confirmar que su API cumple con estos requisitos básicos:
- Cada endpoint se sirve únicamente a través de HTTPS.
- Está implementado OAuth o un flujo de tokens equivalente.
- Los JWT emitidos tienen una fecha de vencimiento.
- Existen límites que protegen todas las rutas, siendo más estrictos en el caso del inicio de sesión.
Se utilizó Express en estos ejemplos, pero las mismas ideas son aplicables a otros frameworks backend, la mayoría de los cuales integran directamente estas herramientas o ofrecen equivalentes nativos. Por ejemplo, NestJS gestiona la validación de solicitudes a través de objetos de transferencia de datos. La documentación de su framework mostrará la versión idiomática de cada capa.
Los mismos principios también constituyen la base para las implementaciones nativas en la nube en plataformas como Azure, Google Cloud y AWS, las cuales añaden sus propios gateways, servicios de identidad y limitación de velocidad gestionada. Las prácticas específicas de la nube merecen un tratamiento aparte, pero las capas mencionadas anteriormente ya ayudan en gran medida a que una API pase una revisión de seguridad.
Puntos clave
- Separar la verificación de credenciales de la autenticación por solicitud: verificar la contraseña una vez y luego confiar en tokens firmados de corta duración.
- La autenticación no es autorización. Validar los alcances y roles, y aún así verificar la propiedad de cada objeto al que accede una solicitud.
- Usar AES-GCM con un IV único por operación para los datos en reposo; reservar la criptografía asimétrica para valores pequeños e intercambio de claves.
Lecturas relacionadas
- Cuando JWT Auth se vuelve estadoful: Un caso a favor de las sesiones del lado servidor en Node — Conozca cómo las listas de bloqueo por revocación y los almacenes de actualización hacen que la autenticación con JWT sea frágil, cómo las sesiones de Express respaldadas por Postgres la simplifican, y dónde siguen siendo útiles los JWT.
- Construyendo autenticación JWT lista para producción en API de Node.js — Cómo hashear contraseñas, emitir JWT de corta duración, agregar tokens de actualización y mecanismos de revocación, separar la autorización, resistir ataques por fuerza bruta y verificar que la autenticación falle correctamente.
- Autenticación biométrica avanzada en iOS y Android con claves vinculadas a hardware — Entienda por qué el Face ID por sí solo no demuestra nada a su servidor, y cómo utilizar las claves del Secure Enclave y Android Keystore para firmar desafíos únicos dirigidos al servidor.
- De la subida a la URL: Almacenamiento y servicio seguro de archivos de usuario en Express — Aprenda dónde deben almacenarse los archivos subidos en las aplicaciones Express, cómo express.static asocia una carpeta con URLs y qué medidas de seguridad evitan que las subidas de los usuarios se conviertan en vulnerabilidades.
- Control de acceso basado en roles en Express con JWT y dos middlewares — Aprenda cómo implementar un control de acceso basado en roles en una API Express combinando el middleware de autenticación JWT con un guardián authorize() reutilizable, y cuándo responder con 401 en lugar de 403.