Resumen
En una instancia de prueba de Burp Suite DAST (Enterprise), de PortSwigger, el endpoint de inicio de sesión no ponía ningún límite propio al tamaño del campo password. El problema no era que aceptase textos largos, sino el orden de las operaciones: la aplicación recibía todo el cuerpo de la petición, lo guardaba en memoria, deserializaba el JSON y creaba la cadena completa, y solo después comprobaba las credenciales y devolvía el error. Es lo contrario del principio fail-fast, según el cual lo que no tiene sentido se descarta lo antes posible.
- Endpoint afectado:
POST /api-internal/login - Acceso: sin autenticación y accesible públicamente
- Tipo: validación de entrada incorrecta (CWE-20)
- Severidad final: baja (el investigador la había propuesto como alta). El título del reporte la marca como fuera del alcance del programa, aunque consta como resuelto y con recompensa (importe no público).
El investigador omitió a propósito el dominio de la instancia, por eso en la prueba de concepto aparece como <target-instance>.
Una contraseña real suele ocupar entre 8 y 128 caracteres. Este endpoint aceptaba valores de varios megabytes. La infraestructura que hay por delante sí cortaba las peticiones más grandes a nivel de red, pero todo lo que quedaba por debajo de ese umbral lo procesaba la aplicación entero.
Pasos de reproducción
- Preparar un cuerpo JSON con un usuario cualquiera y una contraseña de unos 10 MB, y enviarlo al endpoint de login:
curl -X POST https://<target-instance>/api-internal/login \
-H "Content-Type: application/json" \
--data-binary @<(python3 -c '
payload = "A" * (10 * 1024 * 1024)
print(f\'{{"username":"admin","password":"{payload}"}}\')
')
- Observar que la respuesta tarda: el servidor recibe todo el cuerpo, lo parsea y reserva memoria para la cadena antes de contestar.
- Comprobar que, tras ese trabajo, devuelve el fallo de autenticación de siempre, no un error por tamaño:
{"code":2,"error":"Login failed."}
Si la petición supera el límite de la infraestructura, se rechaza antes en la capa de red. Si queda por debajo, la aplicación la procesa entera.
Impacto
El coste es desigual: al atacante le basta con mandar un texto grande, mientras que el servidor tiene que reservar memoria, parsear el JSON, gastar CPU y cargar al recolector de basura. Todo eso ocurre en un endpoint sin autenticación, que es una frontera de seguridad y debería hacer el menor trabajo posible con datos que no son de fiar. El reporte no demuestra una caída del servicio: lo que describe es más consumo de recursos del necesario y más superficie de ataque en un componente crítico.
El investigador resumía así la diferencia entre el flujo que había y el que proponía:
Receive request
↓
Buffer entire request body
↓
Parse JSON payload
↓
Allocate large string
↓
Execute authentication logic
↓
Reject invalid credentials
Receive request
↓
Validate Content-Length
↓
Validate field size constraints
↓
Parse JSON payload
↓
Execute authentication logic
Remediación
El reporte no explica qué cambió PortSwigger; solo consta como resuelto. Lo que proponía el investigador:
- Poner un límite estricto a la longitud de la contraseña en la propia aplicación, por ejemplo 8192 bytes:
if (password.length > 8192) {
return HTTP 400;
}
- Hacer que el proxy inverso o el balanceador devuelva
HTTP 413 Payload Too Largesegún la cabeceraContent-Length. - Comprobar los tamaños de los campos antes de cargar y parsear el cuerpo siempre que se pueda, en vez de depender solo del límite global de la infraestructura.
Qué aprender de este caso
- En endpoints sin autenticación (login, registro, recuperación de contraseña) merece la pena probar campos de texto enormes y medir el tiempo de respuesta: si el error es el mismo de siempre pero tarda más, la aplicación está procesando la entrada antes de validarla.
- Que un proxy corte las peticiones gigantes no basta: entre el límite global de la infraestructura y el tamaño razonable de un campo puede quedar un margen de megas que la aplicación parsea entero.
- La defensa es fijar en la propia aplicación un tamaño máximo de cuerpo para cada ruta y de cada campo, y comprobarlo antes de deserializar el JSON, devolviendo 400 o 413 en lugar de un fallo de autenticación genérico.