Conceptos Básicos de Seguridad
8 ejemplos para empezar con la Seguridad para backends de Node.js: 6 básicos y 2 intermedios.
Busca en todas las páginas de la documentación
8 ejemplos para empezar con la Seguridad para backends de Node.js: 6 básicos y 2 intermedios.
npm install zod express@5 helmet
npm install -D typescript@5.6 tsxLos servicios de Node.js se enfrentan a entradas no confiables de cuerpos HTTP, cadenas de consulta, encabezados, archivos subidos, webhooks y variables de entorno cargadas al inicio.
Rechaza la entrada mal formada antes de que llegue a la lógica de negocio.
import { z } from "zod";
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(120),
});
type CreateUser = z.infer<typeof CreateUserSchema>;
function parseCreateUser(body: unknown): CreateUser {
return CreateUserSchema.parse(body);
}parse lanza un error con datos inválidos; mapea a HTTP 400 en tu manejador de errores.unknown para body fuerza la validación; nunca confíes solo en el tipado de req.body desde el middleware.schemas/ compartido.Relacionado: OWASP Top 10 para APIs - API1 autorización a nivel de objeto rota
Los IDs y filtros de la URL están controlados por el atacante.
import { z } from "zod";
const ListQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
function parseListQuery(query: Record<string, unknown>) {
return ListQuerySchema.parse(query);
}z.coerce maneja los valores de cadena de consulta de Express/Fastify.limit para prevenir DoS a través de ?limit=999999.Relacionado: Paginación y Filtrado - parámetros de lista acotados
Las claves API en el historial de git son para siempre.
import { z } from "zod";
const EnvSchema = z.object({
JWT_SECRET: z.string().min(32),
DATABASE_URL: z.string().url(),
});
export const env = EnvSchema.parse(process.env);parse al inicio falla las implementaciones tempranamente cuando faltan secretos..env en la imagen.env o process.env en los banners de inicio.Relacionado: Conceptos Básicos de Configuración - configuración de doce factores
Los encabezados predeterminados reducen el riesgo de XSS, clickjacking y MIME sniffing.
import express from "express";
import helmet from "helmet";
const app = express();
app.use(helmet());
app.disable("x-powered-by");helmet() establece valores predeterminados sensatos para encabezados comunes.disable("x-powered-by") elimina la huella digital del framework.Relacionado: Encabezados de Seguridad y CORS - reglas CORS de credenciales
Las comprobaciones de identidad y permisos son pasos separados.
type AuthUser = { id: string; role: "admin" | "member" };
function requireAuth(user: AuthUser | undefined): AuthUser {
if (!user) throw new HttpError(401, "Unauthorized");
return user;
}
function requireAdmin(user: AuthUser): void {
if (user.role !== "admin") throw new HttpError(403, "Forbidden");
}user.id === resource.ownerId) en cada acceso al objeto.user; los manejadores aún verifican el acceso a nivel de objeto.Relacionado: Reglas de Seguridad - reglas HTTP básicas
Las trazas de pila pertenecen a los registros, no a las respuestas JSON.
import express from "express";
import pino from "pino";
class HttpError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
const logger = pino({ level: "info" });
const app = express();
app.use((err: unknown, req: express.Request, res: express.Response, _next: express.NextFunction) => {
const status = err instanceof HttpError ? err.status : 500;
const message = status < 500 ? (err as Error).message : "Internal Server Error";
logger.error({ err, path: req.path }, "request_error");
res.status(status).json({ error: message });
});err completo con el serializador Pino { err } para las trazas de pila.ZodError de Zod a 400 con rutas de campo, no mensajes internos.Relacionado: Estándares de Respuesta de Errores - errores de API consistentes
La obtención de URLs arbitrarias permite el acceso a servicios de metadatos internos.
import { lookup } from "node:dns/promises";
import ipaddr from "ipaddr.js";
async function assertPublicUrl(raw: string): Promise<URL> {
const url = new URL(raw);
if (!["http:", "https:"].includes(url.protocol)) throw new Error("Invalid protocol");
const { address } = await lookup(url.hostname);
const parsed = ipaddr.parse(address);
const range = parsed.range();
if (range !== "unicast") throw new Error("Blocked IP range");
return url;
}Relacionado: Protecciones SSRF - implementación completa de la protección
La fusión profunda de JSON no confiable puede contaminar Object.prototype.
function safeAssign<T extends Record<string, unknown>>(
target: T,
source: unknown
): T {
if (source === null || typeof source !== "object" || Array.isArray(source)) {
throw new Error("Invalid merge source");
}
for (const key of Object.keys(source as object)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
(target as Record<string, unknown>)[key] = (source as Record<string, unknown>)[key];
}
return target;
}Object.assign(target, req.body) sin filtrar claves en entradas no confiables.parse de Zod sobre la fusión profunda para dar forma a las solicitudes.lodash y evita _.merge en JSON de usuario.Relacionado: Contaminación de Prototipos - riesgos de lodash y merge
Versiones de la pila: Esta página fue escrita para Node.js 24.18.0 (LTS Activo), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.
Revisado por Chris St. John·Última actualización: 16 jul 2026