Skip to main content

Seguridad básica en Node.js ES Modules

Cuando se empieza a trabajar con Node.js es habitual pensar que la seguridad llegará cuando usemos Express, JWT o bases de datos. Sin embargo, gran parte de la seguridad depende de cómo entendemos el ciclo de una petición HTTP, cómo tratamos los datos que entran en nuestro servidor y qué decisiones tomamos al escribir el código. Aunque todavía no uses frameworks ni bases de datos, es posible construir servicios razonablemente seguros aplicando solo lo que ofrece Node de forma nativa.

Qué es exactamente “seguridad” cuando trabajas con Node.js

La seguridad en Node.js no es una lista de librerías, sino un conjunto de decisiones correctas en cada capa del servidor. La mayoría de los fallos no aparecen por no usar Express, sino por errores como estos:

  • Aceptar datos sin validarlos.
  • Interpretar el cuerpo de la petición sin límites.
  • Guardar contraseñas en texto plano.
  • Responder con información interna del servidor.
  • Escribir en archivos JSON sin control de errores.
  • Devolver mensajes distintos para fallos distintos, revelando datos sensibles.
  • Asumir que cualquier JSON recibido es válido y seguro.

Por tanto, la primera meta antes de usar herramientas avanzadas es aprender a leer, validar, almacenar y responder de forma correcta.

Cómo funciona un servidor HTTP creado con Node.js puro

Cuando no usas Express trabajas directamente con el módulo http. Este enfoque obliga (para bien) a comprender qué llega exactamente al servidor en cada petición.

Ejemplo mínimo:

import http from "node:http";

const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Servidor operativo");
});

server.listen(3000);

Este pequeño fragmento ya ilustra el principal punto de seguridad: nada ocurre automáticamente. No hay parseo de JSON, no existen rutas predefinidas, ni validadores, ni registro de errores. Eso significa que la seguridad depende directamente de lo que implementes.

En un servidor real necesitarás leer el cuerpo de la petición, extraer parámetros, controlar métodos y validar datos.

Control manual de rutas

if (req.method === "POST" && req.url === "/api/usuarios") {
// lógica del endpoint
}

Esta forma explícita de crear rutas mantiene el control sobre lo que realmente aceptas.

Cómo entran los datos en tu servidor y por qué esto es crítico

En Node, el cuerpo de la petición no se recibe como un objeto, sino como un flujo de datos. Esta característica implica dos riesgos:

  • Un cliente puede enviar más datos de los esperados y desbordar la memoria.
  • Puede enviar JSON corrupto o malicioso que provoque errores graves.

El enfoque seguro consiste en implementar tu propio lector del cuerpo con límites y con un manejo de errores adecuado.

Ejemplo didáctico:

export function readJsonBody(req) {
return new Promise((resolve, reject) => {
let data = "";

req.on("data", (chunk) => {
data += chunk;

if (data.length > 1_000_000) {
reject(new Error("Body demasiado grande"));
req.destroy();
}
});

req.on("end", () => {
try {
const json = JSON.parse(data || "{}");
resolve(json);
} catch {
reject(new Error("JSON inválido"));
}
});

req.on("error", reject);
});
}

Este patrón cumple varias funciones esenciales:

  • Limita el tamaño del cuerpo.
  • Rechaza entradas dañadas o mal formadas.
  • Impide que un error de parseo detenga tu servidor.
  • Obliga a que cada llamada esté envuelta en try/catch.

Este tipo de funciones forman la base para recibir datos de forma segura.

Validación y sanitización: decidir qué datos son aceptables

Recibir un JSON no significa que el contenido sea correcto. Validar la entrada es obligatorio incluso antes de tener un sistema de autenticación.

Ejemplo sencillo:

function validarEmail(email) {
return (
typeof email === "string" && email.includes("@") && email.length <= 100
);
}

function validarPassword(pass) {
return typeof pass === "string" && pass.length >= 6 && pass.length <= 200;
}

El objetivo no es hacerlo perfecto, sino evitar valores inesperados, tamaños excesivos y estructuras anómalas.

Al validar reduces:

  • Inyecciones,
  • Corrupciones del archivo JSON,
  • Errores de aplicación,
  • Aperturas involuntarias de información interna.

Almacenar datos de forma segura con JSON

El almacenamiento en JSON es útil para aprender, pero tiene riesgos:

  • No hay control de concurrencia.
  • Es fácil corromper un archivo si algo falla durante una escritura.
  • Los datos pueden llegar mal formados.
  • No existe protección automática ante errores.

Un patrón básico y seguro para trabajar con archivos JSON sería:

import { readFile, writeFile } from "node:fs/promises";

export async function readData(path) {
try {
const content = await readFile(path, "utf8");
return JSON.parse(content);
} catch {
return [];
}
}

export async function writeData(path, data) {
const json = JSON.stringify(data, null, 2);
await writeFile(path, json, "utf8");
}

Estas decisiones proporcionan varias garantías:

  • El servidor no se cae si el archivo está vacío o corrupto.
  • Nunca se escribe nada sin convertirlo previamente a JSON válido.
  • Siempre controlas los errores sin filtrarlos hacia el cliente.

Más adelante, cuando uses SQLite o MongoDB, este mismo enfoque mental seguirá siendo importante.

Contraseñas y datos sensibles: cómo protegerlos sin librerías externas

Aunque todavía no implementemos sesiones o JWT, sí es necesario almacenar contraseñas de forma correcta. Guardarlas en texto plano, incluso en un proyecto básico con JSON, es un error de seguridad crítico.

Node incluye el módulo crypto, suficiente para implementar un hash de contraseña fuerte.

import crypto from "node:crypto";

export function hashPassword(password) {
const salt = crypto.randomBytes(16).toString("hex");
const hash = crypto.scryptSync(password, salt, 64).toString("hex");
return `${salt}:${hash}`;
}

export function verifyPassword(password, stored) {
const [salt, hash] = stored.split(":");
const test = crypto.scryptSync(password, salt, 64);
return crypto.timingSafeEqual(Buffer.from(hash, "hex"), test);
}

Este patrón asegura:

  • Las contraseñas nunca se almacenan tal cual.
  • Cada contraseña genera un hash diferente incluso si el valor es el mismo.
  • La comparación segura evita filtraciones por tiempo de ejecución.

Con esto ya tienes un sistema real y seguro de autenticación básica sin usar ninguna librería.

Cabeceras HTTP: la otra mitad de la seguridad

En Node puro, tú controlas cada cabecera. Esto es una ventaja, pero también una responsabilidad.

Siempre declarar Content-Type correcto:

res.setHeader("Content-Type", "application/json; charset=utf-8");

No revelar información interna:

  • Revelar si un email existe o no es un fallo habitual.
  • Lo correcto es responder siempre con el mismo mensaje genérico:
  • Credenciales no válidas.

Controlar CORS:

Si no se define, el navegador bloqueará peticiones externas.

Un ajuste básico durante el aprendizaje es:

res.setHeader("Access-Control-Allow-Origin", "*");

Más adelante podrás definir una lista de orígenes permitidos.

Manejo seguro de errores: cómo evitar que un fallo derribe el servidor

Un error no capturado puede detener el proceso de Node o mostrar un stacktrace al cliente. Ambos escenarios son peligrosos. Por eso cada endpoint debe envolver su lógica:

try {
// lógica del endpoint
} catch (error) {
console.error("Error interno:", error);
res.statusCode = 500;
res.end(JSON.stringify({ error: "Error interno del servidor" }));
}

Esto garantiza:

  • El cliente siempre recibe un JSON válido.
  • El servidor nunca expone detalles internos.
  • El proceso de Node no se detiene.

Estructura recomendada para una API segura sin frameworks

La estructura mínima para trabajar de forma segura sería:

  • Un servidor creado con http.
  • Un módulo con funciones para leer el cuerpo con límite.
  • Un módulo de validadores.
  • Un módulo para leer y escribir JSON.
  • Un módulo para gestionar contraseñas.
  • Rutas manuales basadas en método y URL.
  • Respuestas uniformes y siempre en JSON.
  • Bloques try/catch envolviendo cada operación importante.

Con esta base ya puedes construir un CRUD completo, login seguro y validación de datos sin necesidad de pasar a Express.

Por qué este enfoque es limitado pero adecuado en esta fase

Trabajar con JSON tiene limitaciones:

  • No escala para muchos usuarios.
  • No existe bloqueo de escritura.
  • El archivo puede corromperse si la escritura falla.
  • No soporta transacciones ni consultas avanzadas.

Protección contra rutas no permitidas y control de acceso básico

Cuando trabajas con Node.js puro, no existe ningún router automático que impida que el usuario acceda a rutas internas o no deseadas. Por eso es importante definir claramente qué rutas son válidas y responder de forma segura cuando alguien solicita algo que tu servidor no ofrece.

Una forma simple de hacerlo es crear un conjunto de rutas válidas y comprobar manualmente si la petición forma parte de ese conjunto. Si no lo es, debes responder con un código adecuado y un mensaje genérico.

Ejemplo básico:

const rutasPublicas = new Set(["/api/usuarios", "/api/login", "/api/registro"]);

function rutaValida(url) {
return rutasPublicas.has(url);
}

En el servidor:

if (!rutaValida(req.url)) {
res.statusCode = 404;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: "Recurso no disponible" }));
return;
}

Esta técnica evita:

  • Exponer rutas internas que quizás no deberían usarse aún
  • Que un usuario pueda pedir un archivo o endpoint no previsto
  • Que respondas con una página HTML por defecto o mensajes del servidor

Para proyectos más grandes puedes usar expresiones regulares, pero en este punto es suficiente con listas o conjuntos.

Control básico de métodos HTTP permitidos

Además de controlar las rutas, debes decidir qué métodos HTTP son aceptables. Un error común al comenzar es permitir automáticamente cualquier método, incluso aquellos que nunca se necesitan como DELETE o OPTIONS.

Un control básico consiste en especificar qué métodos acepta cada ruta y rechazar los demás.

Ejemplo simple:

const metodosPermitidos = {
"/api/usuarios": ["GET", "POST"],
"/api/login": ["POST"],
"/api/registro": ["POST"],
};

function metodoValido(url, metodo) {
const permitidos = metodosPermitidos[url];
return permitidos && permitidos.includes(metodo);
}

En el servidor:

if (!metodoValido(req.url, req.method)) {
res.statusCode = 405;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: "Método no permitido" }));
return;
}

Beneficios:

  • Protege de llamadas inesperadas
  • Evita estados no previstos en los datos
  • Reduce la superficie de ataque

Este control es especialmente importante cuando más adelante empieces a registrar usuarios o gestionar información sensible.

Validación segura de parámetros en la URL

Cuando un cliente envía una petición como:

GET /api/usuarios?id=123

Node no interpreta los parámetros automáticamente. Como debes hacerlo tú, también debes validarlos tú. Esto incluye:

  • Comprobar que exista la clave esperada
  • Validar su tipo
  • Controlar su longitud
  • Evitar caracteres inesperados

Node ofrece el módulo URL para parsear correctamente cualquier parámetro:

const url = new URL(req.url, `http://${req.headers.host}`);
const id = url.searchParams.get("id");

Después, validas:

function validarId(id) {
return /^[a-zA-Z0-9-_]{1,50}$/.test(id);
}

if (!validarId(id)) {
res.statusCode = 400;
res.end(JSON.stringify({ error: "Parámetro inválido" }));
return;
}

Este patrón previene:

  • Inyecciones
  • Acceso a datos incorrectos
  • Errores al leer JSON
  • Pérdida de integridad en los datos

Puedo continuar con:

  • Rate limiting básico
  • Seguridad al servir archivos estáticos
  • Prevención de Directory Traversal
  • Generación de tokens simples con crypto
  • Logging seguro
  • Estructura modular segura
  • Protección ante ataques por flooding o fuerza bruta

Control básico de velocidad (rate limiting) para evitar abusos

Cuando un servidor permite enviar peticiones sin límite, un atacante puede saturarlo enviando cientos o miles de solicitudes en muy poco tiempo. Aunque existen librerías externas para este propósito, es posible implementar un sistema básico utilizando únicamente Node.js puro y un objeto en memoria.

La idea es sencilla: registrar cada IP que hace una petición y contar cuántas peticiones ha realizado en un intervalo corto. Si supera un límite, se bloquea temporalmente.

Ejemplo simple:

const limites = new Map();

function controlarRateLimiting(ip) {
const ahora = Date.now();
const ventanaTiempo = 10_000;
const maxPeticiones = 50;

if (!limites.has(ip)) {
limites.set(ip, { contador: 1, inicio: ahora });
return true;
}

const info = limites.get(ip);

if (ahora - info.inicio > ventanaTiempo) {
limites.set(ip, { contador: 1, inicio: ahora });
return true;
}

info.contador++;

if (info.contador > maxPeticiones) {
return false;
}

return true;
}

Uso en el servidor:

const ip = req.socket.remoteAddress;

if (!controlarRateLimiting(ip)) {
res.statusCode = 429;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: "Demasiadas peticiones" }));
return;
}

Esto protege contra:

  • Abuso involuntario del cliente
  • Intentos de denegación de servicio a pequeña escala
  • Scripts automatizados llamando cientos de veces a la API

No es un sistema profesional, pero sí una medida adecuada para aprender.

Prevención de ataques por flooding y consumo excesivo de recursos

Además del rate limiting, un atacante puede intentar saturar el servidor enviando peticiones incompletas o manteniendo conexiones abiertas. Node.js es relativamente eficiente ante estos ataques, pero aún así conviene implementar medidas básicas.

Algunas recomendaciones esenciales:

  • Definir un límite máximo de tamaño del cuerpo (esto ya lo hicimos con el lector de JSON).
  • Cerrar conexiones inactivas si pasan demasiado tiempo sin completar la petición.
  • Usar req.destroy() cuando se detecte comportamiento sospechoso.
  • Evitar operaciones costosas en CPU dentro del servidor.

Ejemplo simple de timeout:

req.setTimeout(5000, () => {
req.destroy();
});

Esto evita que un atacante mantenga peticiones abiertas más tiempo del necesario.

Cómo servir archivos estáticos de forma segura

En Node puro puedes servir HTML, CSS o imágenes leyendo archivos desde disco, pero hacerlo de manera ingenua es peligroso. Por ejemplo, si el usuario solicita:

/public/../../etc/passwd

y el servidor concatena rutas sin validarlas, podría exponer información del sistema.

Para evitarlo, hay que:

  • Limitar la carpeta desde la cual se pueden servir archivos.
  • Normalizar rutas con path.normalize.
  • Comprobar que la ruta final sigue dentro del directorio permitido.

Ejemplo:

import { readFile } from "node:fs/promises";
import path from "node:path";

const raizPublica = path.resolve("public");

async function servirArchivoSeguro(req, res) {
const urlPath = decodeURIComponent(req.url.replace("/public", ""));
const rutaAbsoluta = path.normalize(path.join(raizPublica, urlPath));

if (!rutaAbsoluta.startsWith(raizPublica)) {
res.statusCode = 403;
res.end("Acceso denegado");
return;
}

try {
const contenido = await readFile(rutaAbsoluta);
res.statusCode = 200;
res.end(contenido);
} catch {
res.statusCode = 404;
res.end("Archivo no encontrado");
}
}

Esto protege contra ataques de tipo Directory Traversal.

Prevención explícita de Directory Traversal

El ataque Directory Traversal consiste en intentar salir del directorio permitido mediante rutas como:

/public/../
/public/../../config

La técnica anterior lo previene, pero conviene explicar la idea:

  • Nunca concatenes rutas sin normalizarlas.
  • Nunca permitas acceso a rutas dinámicas sin revisar su contenido.
  • Siempre verifica que al unir la ruta base con la ruta solicitada, el resultado siga dentro del directorio permitido.

Cualquier sistema de archivos seguro se basa en este patrón.

Generación de tokens simples con crypto (sin JWT)

Aunque aún no uses JWT, es útil aprender cómo generar un token seguro utilizando únicamente el módulo crypto. Estos tokens sirven para:

  • Verificaciones de email
  • Tokens de recuperación
  • Identificadores internos
  • Claves de sesión simples para pruebas

Ejemplo:

import crypto from "node:crypto";

function generarTokenSimple() {
return crypto.randomBytes(32).toString("hex");
}

Si quieres un token firmado:

function generarTokenFirmado() {
const clave = "clave-secreta-muy-larga";
const valor = crypto.randomBytes(16).toString("hex");
const firma = crypto.createHmac("sha256", clave).update(valor).digest("hex");

return `${valor}.${firma}`;
}

function verificarTokenFirmado(token) {
const clave = "clave-secreta-muy-larga";
const [valor, firma] = token.split(".");
const firmaEsperada = crypto
.createHmac("sha256", clave)
.update(valor)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(firma), Buffer.from(firmaEsperada));
}

Más adelante podrás comparar esto con JWT, pero por ahora es suficiente.

Logging seguro y protección de mensajes internos

Un error muy frecuente es imprimir en consola mensajes demasiado detallados o incluso datos sensibles. El logging seguro implica:

  • Registrar errores en el servidor, pero nunca enviarlos al cliente.
  • Evitar imprimir contraseñas, tokens, JSON completos o información privada.
  • Usar mensajes claros pero genéricos para el cliente.
  • Controlar qué se registra en modo desarrollo y producción.

Patrón básico:

try {
// código
} catch (error) {
console.error("Error interno:", error.message);
res.statusCode = 500;
res.end(JSON.stringify({ error: "Error interno del servidor" }));
}

Para el cliente, mensaje genérico.

Para ti, mensaje útil pero no peligroso.

Estructura modular segura para proyectos sin frameworks

A medida que el documento crece, conviene estructurar el proyecto para evitar errores. Una estructura recomendable sería:

  • /servidor.js: entrada principal con http.createServer
  • /rutas/: módulos que gestionan rutas
  • /utils/: validadores, funciones de lectura JSON, hashing, rate limiting
  • /data/: archivos JSON
  • /public/: archivos estáticos

Ventajas:

  • Cada módulo tiene una responsabilidad clara.
  • Es más fácil controlar permisos, accesos y lógica.
  • Evitas mezclar lectura de JSON con lógica de rutas.
  • Facilita detectar errores y mejorar la seguridad.

Protección básica ante ataques de fuerza bruta

Un ataque de fuerza bruta ocurre cuando alguien intenta adivinar una contraseña enviando muchas peticiones seguidas. Aunque todavía no uses JWT ni sistemas de autenticación avanzados, sí puedes aplicar medidas simples para evitar este tipo de ataque con Node.js puro.

Métodos básicos para limitar la fuerza bruta:

  • Controlar cuántos intentos de login hace una misma IP.
  • Introducir pequeños retardos tras varios intentos fallidos.
  • No revelar si el usuario existe o no.
  • Registrar los errores sin mostrar información interna.

Ejemplo muy sencillo basado en memoria:

const intentosFallidos = new Map();

function registrarIntentoFallido(ip) {
const ahora = Date.now();
const ventana = 15 * 1000;
const maxIntentos = 5;

if (!intentosFallidos.has(ip)) {
intentosFallidos.set(ip, { contador: 1, inicio: ahora });
return true;
}

const info = intentosFallidos.get(ip);

if (ahora - info.inicio > ventana) {
intentosFallidos.set(ip, { contador: 1, inicio: ahora });
return true;
}

info.contador++;

if (info.contador > maxIntentos) {
return false;
}

return true;
}

En el servidor:

if (!registrarIntentoFallido(ip)) {
res.statusCode = 429;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: "Demasiados intentos. Intente más tarde." }));
return;
}

Esto no detiene ataques avanzados, pero sí frena intentos ingenuos o automatizados.

Protección ante fuzzing simple

El fuzzing es un método mediante el cual un atacante envía datos aleatorios, mal formados o extremadamente grandes para intentar romper el servidor. Aunque no necesitas defenderte al nivel profesional, sí puedes aplicar técnicas básicas:

  • Validar todos los campos recibidos, incluso si crees que siempre serán correctos.
  • Rechazar peticiones cuyo cuerpo supere el límite configurado.
  • Rechazar JSON mal formados.
  • No asumir nunca que un campo existe.
  • No permitir estructuras anidadas muy profundas.

Ejemplo simple para limitar la profundidad del objeto JSON:

function profundidadMaxima(obj, limite = 5) {
if (limite < 0 || typeof obj !== "object" || obj === null) return false;
return Object.values(obj).every((v) => profundidadMaxima(v, limite - 1));
}

Uso:

if (!profundidadMaxima(json)) {
res.statusCode = 400;
res.end(JSON.stringify({ error: "Estructura JSON demasiado profunda" }));
return;
}

Esto ayuda a evitar ataques que intentan hacer que tu aplicación recorra estructuras enormes o cíclicas.

Minimizar la exposición de información del servidor

Cuando se empieza con Node.js es habitual agregar cabeceras, logs extensos o información interna en las respuestas sin darse cuenta. Esto puede permitir a un atacante identificar:

  • versión del servidor
  • sistema operativo
  • estructura interna del proyecto
  • rutas internas
  • fallos de implementación

Buenas prácticas esenciales:

  • No incluir mensajes como “No existe el usuario con ID X”.
  • No devolver la estructura interna del JSON en una respuesta de error.
  • No filtrar stacks de error al cliente.
  • No enviar valores sensibles como tokens o contraseñas incluso en modo prueba.
  • Mantener las respuestas de error siempre iguales sin distinguir demasiado.

Ejemplo de respuesta demasiado detallada:

{
error: "El usuario con email test@example.com no existe";
}

Respuesta segura:

{
error: "Credenciales no válidas";
}

Uso de tiempos constantes para comparaciones sensibles

Cuando trabajas con contraseñas, tokens o firmas digitales, compararlos directamente con === puede revelar información mediante análisis temporal. Node proporciona crypto.timingSafeEqual para evitar estas fugas de tiempo.

Ejemplo inseguro:

if (passwordHash === storedHash) { ... }

Ejemplo correcto:

import crypto from "node:crypto";

crypto.timingSafeEqual(Buffer.from(passwordHash), Buffer.from(storedHash));

Buena parte de la seguridad moderna depende de estas pequeñas decisiones.

Limpieza y aislamiento de módulos internos

La organización del código influye en la seguridad. Para mantener el servidor limpio y reducir el riesgo de errores:

  • Mantén el lector de JSON en un módulo separado.
  • Mantén el validador de entrada separado de las rutas.
  • Mantén la lógica de lectura/escritura de JSON en otro módulo.
  • Mantén los hashes de contraseñas en un módulo seguro.
  • Mantén controladores de rutas pequeños y simples.

Beneficios:

  • Menos probabilidad de mezclar datos sensibles accidentalmente.
  • Menor superficie de ataque.
  • Más fácil verificar fallos o inconsistencias.
  • Permite ampliar funcionalidades progresivamente sin romper nada.

Ejemplo de estructura:

servidor.js
rutas/
usuarios.js
login.js
utils/
leerJSON.js
escribirJSON.js
leerBody.js
validar.js
hash.js
seguridad/
rateLimit.js
fuerzaBruta.js
tokens.js
data/
usuarios.json
public/
index.html

Este tipo de división no solo es orden, sino también seguridad.

Seguridad en operaciones síncronas vs asíncronas

En Node.js algunas operaciones pueden bloquear el hilo principal si son síncronas. El bloqueo del event loop es una forma de ataque que puede dejar el servidor congelado. Para evitar esto:

  • No usar operaciones síncronas en rutas críticas (ejemplo: readFileSync).
  • No implementar funciones que consuman mucha CPU dentro del servidor.
  • Siempre preferir las versiones asíncronas de fs.
  • Mantener la lógica de hashing fuera de bucles intensivos.

Un servidor lento es un servidor vulnerable.

Cierre conceptual: cómo pensar la seguridad en Node.js puro

Trabajar sin frameworks, sin bases de datos y sin librerías externas es, paradójicamente, la mejor manera de entender la seguridad real. Cuando usas Express o JWT, estas herramientas ya hacen parte del trabajo por ti. Pero aprender a manejar lo siguiente desde cero:

  • Control de rutas
  • Control de métodos
  • Validación y saneamiento
  • Lectura manual del cuerpo
  • Limites de tamaño
  • Protección del sistema de archivos
  • Hashing de contraseñas
  • Protección ante sesiones repetidas
  • Prevención de flooding
  • Rate limiting básico
  • Tokens firmados simples.

Seguro que ahora sí tienes un documento extenso, claro, y con todas las bases necesarias para la seguridad básica en Node.js sin frameworks.