Endurece una API de Node en producción: CORS correcto, rate limiting, cabeceras de seguridad y defensas contra los principales riesgos de la OWASP API.
Abrir esta lección en KodokonOWASP mantiene un top 10 dedicado a las APIs. Los tres riesgos dominantes: BOLA (Broken Object Level Authorization - acceder al objeto de otro usuario cambiando un id), Broken Authentication (tokens mal validados, secretos débiles) y Unrestricted Resource Consumption (sin límite de tasa, tamaño o tiempo). Fíjate en que ninguno de los tres se soluciona instalando una biblioteca: son propiedades de tu diseño. Empecemos por los mecanismos transversales: CORS, rate limiting y cabeceras.
import { createServer } from "node:http";
const allowed = new Set(["https://app.example.com"]);
const server = createServer((req, res) => {
const origin = req.headers.origin;
if (origin && allowed.has(origin)) {
res.setHeader(
"Access-Control-Allow-Origin",
origin,
);
res.setHeader("Vary", "Origin");
}
if (req.method === "OPTIONS") {
res.setHeader(
"Access-Control-Allow-Methods",
"GET,POST,PUT,DELETE",
);
res.setHeader(
"Access-Control-Allow-Headers",
"content-type,authorization",
);
res.setHeader("Access-Control-Max-Age", "86400");
res.writeHead(204).end();
return;
}
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ ok: true }));
});
server.listen(3000);Entiende claramente lo que CORS no es: una protección del servidor. curl o un backend de terceros ignoran por completo estas cabeceras; solo el navegador las aplica, para proteger a sus usuarios. Tres reglas de experto: devuelve el origen exacto desde una lista de permitidos (nunca reenvíes a ciegas la cabecera Origin), añade Vary: Origin para no envenenar las cachés intermedias, y ten en cuenta que el comodín * es rechazado por los navegadores en cuanto la petición lleva cookies (credentials).
const WINDOW_MS = 60_000;
const LIMIT = 100;
const hits = new Map();
function check(ip) {
const now = Date.now();
const entry = hits.get(ip);
if (!entry || now - entry.start >= WINDOW_MS) {
hits.set(ip, { start: now, count: 1 });
return { ok: true, remaining: LIMIT - 1 };
}
entry.count += 1;
const remaining = Math.max(LIMIT - entry.count, 0);
return { ok: entry.count <= LIMIT, remaining };
}
console.log(check("203.0.113.7"));Las cabeceras de seguridad cuestan unas pocas líneas y bloquean clases enteras de ataques: HSTS fuerza HTTPS para las visitas futuras, X-Content-Type-Options: nosniff impide que el navegador adivine un tipo MIME, y una CSP restrictiva (default-src 'none') neutraliza la explotación de una respuesta de API renderizada por error en un navegador. Elimina también cualquier cabecera que revele tu stack: Express añade X-Powered-By por defecto - desactívalo con app.disable('x-powered-by').
function setSecurityHeaders(res) {
res.setHeader(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains",
);
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader(
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'",
);
res.setHeader("Cache-Control", "no-store");
}
export { setSecurityHeaders };