Skip to content
This page has been auto-translated and may contain errors.View in English

Denegación de servicio

El formulario de registro tiene un campo de nombre. Nada impide que un nombre tenga diez millones de caracteres.

js
// Lo que se envía como `name`
'a'.repeat(10_000_000)

Nada en esa cadena es código. No se va a interpretar como nada en particular. Simplemente es enorme, y resulta que con ser enorme basta.

Guárdalo y la base de datos crece. Consúltalo y la consulta se vuelve lenta. Respáldalo y el respaldo también crece, cada noche, para siempre. Repite eso unas cuantas miles de veces y el servicio deja de poder responderle a nadie.

Una solicitud no tiene que ser ingeniosa para hacer daño. Solo tiene que ser costosa.

Envía esto solo contra tu propio servicio

Las pruebas de volumen son indistinguibles de un ataque mientras están corriendo, y afectan a todos los demás usuarios de aquello contra lo que se dirijan. Úsalas solo en tu propia infraestructura o en un sistema para el que tengas permiso explícito de hacer pruebas.

La disponibilidad es una propiedad de seguridad

La seguridad suele discutirse en términos de mantener secretos y mantener los datos correctos. Existe una tercera propiedad, y este capítulo trata de ella: la disponibilidad, es decir, si el servicio responde o no.

Un ataque de denegación de servicio, DoS por sus siglas en inglés, va tras esa tercera propiedad. No lee nada, no cambia nada ni roba nada. Lo que hace es dejar al servicio incapaz de cumplir su función, lo cual para un negocio suele ser el resultado más costoso.

JunoLa disponibilidad es una propiedad de seguridad Los ataques suelen imaginarse como robos, así que este puede sentirse como que no cuenta. Al fin y al cabo, no se lleva nada.

Piensa en lo que pierde una tienda el día que no puede abrir. Ahí tampoco se robó nada.

JunoLa disponibilidad es una propiedad de seguridad Este tipo de ataque se ve distinto en una revisión de código. No hay una función peligrosa que buscar, ni un punto donde un valor se convierta en código. La pregunta aquí es de costo: para cada entrada, ¿cuál es lo más caro que una solicitud puede hacer que el servidor ejecute, y qué impide que alguien lo pida una y otra vez?

Cada campo sin límite, cada endpoint de listas sin paginación, cada operación de archivo síncrona es una respuesta a esa pregunta.

JunoLa disponibilidad es una propiedad de seguridad Las tres propiedades suelen nombrarse juntas como confidencialidad, integridad y disponibilidad. La disponibilidad es la que el equipo de infraestructura diseña y el equipo de aplicación olvida.

Eso es lamentable, porque los ataques de denegación de servicio más baratos casi siempre son bugs de la aplicación y no volumen de tráfico.

Una sola solicitud que dispara un escaneo completo de tabla sin índice, o una exportación que carga en memoria un año de registros, causa más daño por paquete que cualquier inundación de tráfico.

El tráfico lo puedes absorber gastando dinero. Un endpoint que cuesta diez segundos de CPU por llamada hay que arreglarlo sí o sí.

Cuatro formas en que un servicio cae

El curso agrupa estos ataques según qué explota el atacante, y vale la pena mantener esa clasificación porque cada uno se combate de manera distinta.

FormaCómo funcionaQué agota
Bala mágicaUn solo mensaje construido con precisión que viola una regla del protocolo, como enviar más datos de los que un campo fue diseñado para contenerUn defecto específico, de inmediato
Basado en volumenSuficiente tráfico o una entrada lo bastante grande como para saturar la conexión o llenar el discoAncho de banda, memoria, almacenamiento
De protocoloAbuso de cómo está especificado el funcionamiento de un protocolo, de modo que el servidor retiene recursos que nunca llega a liberarTablas de conexión, temporizadores, sockets
DistribuidoEl mismo tráfico, pero llegando desde miles de máquinas distintas a la vezTodo lo anterior, desde todas partes

Vale la pena ver en detalle el de protocolo, porque muestra qué tan poco tráfico necesita un ataque efectivo. Una conexión TCP, el apretón de manos que subyace a la mayoría del tráfico de internet, se abre en tres pasos:

  1. El cliente envía un paquete SYN, que significa "quisiera conectarme".
  2. El servidor responde con SYN-ACK, que significa "adelante", y empieza a mantener un espacio reservado mientras espera.
  3. El cliente responde con ACK, y la conexión queda establecida.

En una inundación SYN, el atacante envía el primer paso con una dirección de retorno falsificada. El segundo paso va a dirigirse a una máquina que nunca lo pidió y que no tiene nada que responder. El tercer paso nunca llega.

El servidor mantiene un espacio abierto y un temporizador corriendo, mientras el atacante ya pasó a la siguiente dirección falsificada.

Nada de esto implica un volumen alto. Es barato para quien lo envía y costoso para quien lo recibe, una y otra vez, hasta que la tabla de conexiones se llena y un visitante real no consigue un espacio.

Una denegación de servicio distribuida, o DDoS, es cualquiera de estos ataques proveniente de una botnet: una red de máquinas comunes controladas por el atacante, normalmente infectadas sin que sus dueños se den cuenta.

Eso es lo que lo hace difícil de combatir. Cada máquina es un dispositivo real que hace solicitudes que parecen legítimas, así que no hay una sola dirección que bloquear ni una firma que filtrar.

JunoCuatro formas en que un servicio cae El patrón que comparten los cuatro es que el atacante gasta poco y el servidor gasta mucho.

Un apretón de manos falsificado cuesta un solo paquete de enviar y bloquea un espacio durante segundos. Ese desequilibrio es todo el juego, y por eso el volumen no es lo único que hay que vigilar.

JunoCuatro formas en que un servicio cae De los cuatro, los dos que te corresponden como desarrollador de aplicaciones son la bala mágica y el ataque basado en volumen. Los ataques de protocolo y los distribuidos se combaten en el borde de la red, ya sea por tu proveedor de hosting o por alguien que esté delante de ti.

Esa distinción es útil cuando comienza un incidente. Si las solicitudes están llegando y tu servicio se está ahogando con ellas, el problema es tuyo. Si las conexiones ni siquiera se están completando, el problema está en una capa por debajo de la tuya y hay que hacer otra llamada.

JunoCuatro formas en que un servicio cae La categoría que la tabla no nombra, y la que más probablemente esté en tu código, es la algorítmica. Algunas entradas son baratas de enviar y superlineales de procesar, así que el costo sube mucho más rápido que el tamaño de la entrada.

Las expresiones regulares suelen ser las culpables. Contra el patrón /^([a-z0-9]+-?)+$/, darle una secuencia de letras seguida de un signo de exclamación obliga al motor a probar todas las formas posibles de dividir la cadena.

Medido en Node 24, con una llamada por proceso nuevo: 21 caracteres tardan 38 milisegundos, 25 tardan 606 milisegundos, 29 tardan 9.8 segundos, 33 tardan 105 segundos. Cuatro caracteres adicionales multiplican el tiempo por diez aproximadamente, a partir de una solicitud tan pequeña que ningún límite de tamaño la marcaría como sospechosa.

El nombre para esto es ReDoS, denegación de servicio por expresión regular. Los cuantificadores anidados, una repetición dentro de otra repetición, son la forma que hay que buscar, y la solución suele ser reescribir el patrón en lugar de limitar la entrada.

Las defensas se combinan

Aquí no existe un control único. El curso enumera seis, y están ubicados a propósito en capas distintas:

  • Redundancia. Ten más de uno de todo: servidores, centros de datos, rutas de red, servidores de nombres. La regla general es tres, para que uno pueda estar fallando y otro bajo ataque mientras un tercero sigue funcionando.
  • Limitación y regulación de tasa. La limitación de tasa (rate limiting) rechaza las solicitudes que superan un tope. La regulación (throttling) las ralentiza. Cien llamadas a la API por minuto, cinco intentos de inicio de sesión en diez minutos, un megabyte por segundo.
  • Filtrado. Decide qué aceptar y desde dónde. Bloquea direcciones o regiones, rechaza solicitudes con cargas útiles obviamente maliciosas, exige autenticación, revisa los encabezados.
  • Endurecimiento (hardening). Elimina lo que no necesitas. Cada servicio en ejecución es una puerta de entrada, y también lo es cada cuenta que nadie usa: logins de administrador por defecto, personas que se fueron y conservaron el acceso, permisos otorgados una sola vez para una migración.
  • Aplicación de parches. El ataque de bala mágica depende de un defecto específico y conocido. Aplicar la corrección del proveedor es lo que lo elimina.
  • Monitoreo. No puedes detectar lo anormal sin conocer lo normal. Un servicio que normalmente recibe mil solicitudes por hora y de pronto recibe cien mil solo es obviamente anómalo si alguien registró ese dato de las mil.

Para el caso específico del nombre de diez millones de caracteres, la solución es un límite de longitud, verificado en el servidor antes de guardar el valor. Eso es validación de esquemas, y es la capa hacia la que apunta esta sección.

JunoLas defensas se combinan Ninguna de estas seis defensas es la solución por sí sola, y precisamente por eso son seis.

La redundancia te da tiempo. Los límites de tasa acotan el daño. El monitoreo es cómo te enteras. La aplicación de parches elimina el hueco específico. Cada una cubre un momento distinto, así que conviene tenerlas todas en lugar de apostar por la mejor.

JunoLas defensas se combinan El monitoreo es el que los equipos suelen saltarse y luego lamentan, porque es el único que resulta inútil de manera retroactiva. No puedes establecer cómo se veía lo normal después de que el incidente ya empezó.

La base de referencia que vale la pena tener es poco vistosa: solicitudes por minuto por endpoint, percentiles de tiempo de respuesta, tasa de error. Regístralos antes de necesitarlos, y una alerta se vuelve posible cuando el tráfico se duplica, en lugar de cuando el servicio ya colapsó.

JunoLas defensas se combinan Vale la pena conocer los límites de tamaño de cuerpo en Express antes de tener que usarlos, porque fallan de forma silenciosa.

Un express.json({ limit: '32kb' }) global consume el cuerpo y rechaza los que exceden el límite con un 413 Payload Too Large antes de que se ejecute cualquier parser a nivel de ruta. Un express.json({ limit: '2mb' }) agregado después en una ruta específica no hace nada: body-parser ya asignó req._body, así que el segundo parser cree que el trabajo ya está hecho.

Verificado en Express 4 y 5.2.1. La ruta de carga a la que le diste un límite mayor sigue siendo rechazada por el límite global, y el patrón que funciona es no tener ningún parser global, con cada ruta definiendo el suyo propio.

Ponlo a prueba

El formulario de registro acepta name, email y password, los guarda, y devuelve los dos primeros. No hay límites en ningún lado.

Resuelve estas tres cosas:

  1. ¿Qué solicitud individual le cuesta más al servidor, y qué la hace costosa?
  2. ¿Cuáles de las seis defensas detendrían esa solicitud, y cuáles solo reducirían el daño?
  3. ¿Cuál es el cambio más barato que cierra esa brecha?
Compara tus respuestas

1. La solicitud más costosa. Un name o password muy largo. Cuesta memoria al procesarlo, almacenamiento al guardarlo, tiempo en cada consulta que toque esa fila, y espacio en cada respaldo a partir de entonces.

password es el peor de los dos si la aplicación lo hashea. El hasheo es deliberadamente lento, así que un valor enorme convierte una operación costosa en una mucho más costosa todavía.

2. Qué defensas hacen efecto.

  • La detienen por completo: el filtrado y un límite de longitud. El servidor rechaza la solicitud antes de hacer el trabajo costoso.
  • Reducen el daño: la limitación y la regulación de tasa. Acotan qué tan seguido se repite, pero la solicitud individual de todas formas llega.
  • Ninguna de las dos cosas: la redundancia, la aplicación de parches y el monitoreo. Te ayudan a sobrevivirlo, eliminan huecos no relacionados, y te permiten enterarte de que está pasando.

3. El cambio más barato. Una longitud máxima en cada campo de texto, aplicada en el servidor. Es una línea por campo en un esquema y elimina toda la categoría de problema, por eso la sección dedica sus capítulos restantes a lograr que ese esquema quede bien hecho.

Hacia dónde va esto ahora

Dos capítulos después, y ambos bugs vienen de un mismo paso que faltó. Nada verificó lo que llegaba antes de que la aplicación actuara sobre ello.

Inyección SQL es el tercero y el más directo. Entradas que ya no solo se guardan en la base de datos, sino que le dicen qué hacer.