Dos malas prácticas que por separado se toleran y juntas abren la puerta: el token viaja en la URL, y el token no caduca.
De un parámetro en la dirección a una cuenta de administrador abierta a internet
Lo primero que apareció al revisar la aplicación no fue un error de programación, sino una decisión de diseño: todas las peticiones a la API llevaban el token de seguridad dentro de la propia dirección, como un parámetro más. Una llamada tenía esta forma: http://api.example.com/me?token=<tu_token>. El token es la llave de la sesión, e iba a la vista de cualquiera que pudiera leer esa dirección.
Un token en la URL no se queda en la URL. Acaba escrito en los registros del servidor, guardado en el historial del navegador e indexado por los buscadores, y desde cualquiera de esos tres sitios se filtra. El catálogo CWE recoge esta clase de fallo como CWE-598, "Use of HTTP Request With Sensitive Query String". El OWASP Top 10:2025 la sitúa en A07, "Authentication Failures", entre las condiciones que hacen vulnerable a una aplicación: la que "expone el identificador de sesión en la URL, en un campo oculto o en otra ubicación insegura accesible para el cliente".
El segundo hallazgo fue que esos tokens no caducaban. Una vez emitido, un token valía para siempre: ni vencimiento por tiempo ni invalidación por inactividad. Por sí solo ese defecto no abre ninguna puerta, y por eso suele aparcarse detrás de cosas más urgentes; es CWE-613, "Insufficient Session Expiration", y la misma categoría de OWASP recoge las aplicaciones que no invalidan correctamente las sesiones ni los tokens de autenticación al cerrar sesión o tras un periodo de inactividad, una línea que OWASP escribe pensando sobre todo en los tokens de inicio de sesión único. Combinado con el anterior deja de ser un detalle, porque convierte cualquier copia antigua de una dirección en una credencial que todavía funciona.
Para saber si eso era teoría o ya había ocurrido, el equipo vigiló las zonas de internet accesibles públicamente donde una dirección de ese tipo podría haber quedado registrada. Ahí apareció la prueba: una dirección de la API con el token de un usuario real dentro, de la forma http://api.example.com/analytics?token=<token>, ya rastreada y guardada por un archivo web de terceros, de los que conservan copias históricas de páginas públicas.
Se probó ese token contra la aplicación. Seguía siendo válido, pertenecía a un cliente real y tenía privilegios de administrador. Dicho de otra forma: cualquier persona de internet que llegara a esa copia archivada entraba como administrador, sin autenticarse en ningún momento y sin escribir ninguna contraseña.
RECONSTRUCCIÓN · SIN DATOS DE CLIENTE
Cómo un token en la dirección acabó siendo una cuenta de administrador
- CWE-598
- CWE-613
-
La decisión de diseño
GET /me?token=EXAMPLE-TOKEN-A1B2C3D4 HTTP/1.1 Host: api.example.com Accept: application/jsonLa llave de la sesión, escrita en la línea de petición.
Todas las llamadas lo llevaban. No es un error de programación: es una decisión de diseño.
-
Una URL no se queda en la URL
-
REGISTRO DEL SERVIDOR
"GET /me?token=EXAMPLE-TOKEN-A1B2C3D4 ..." -
HISTORIAL DEL NAVEGADOR
api.example.com/me?token=EXAMPLE-TOKEN-A1B2C3D4 -
RASTREADOR PÚBLICO
api.example.com/analytics?token=EXAMPLE-TOKEN-A1B2C3D4
Se filtra desde cualquiera de los tres. El tercero está fuera del control del cliente.
-
REGISTRO DEL SERVIDOR
-
La copia archivada
ARCHIVO WEB DE TERCEROS
http://api.example.com/analytics?token=EXAMPLE-TOKEN-A1B2C3D4Una copia histórica de una página pública. No se puede retirar a voluntad.
-
Reproducido
GET /analytics?token=EXAMPLE-TOKEN-A1B2C3D4 HTTP/1.1 Host: api.example.com Accept: application/jsonemitidoSIN CADUCIDAD · SIN CIERRE POR INACTIVIDAD (CWE-613)
El tiempo no cierra esta puerta.
Seguía siendo válido. Cliente real. Administrador.
Sin técnica de explotación ni herramientas. Bastaba con abrir una dirección que ya estaba publicada.
Lo que se recomendó
GET /me?token=EXAMPLE-TOKEN-A1B2C3D4 HTTP/1.1
GET /me HTTP/1.1
Host: api.example.com
Authorization: Bearer EXAMPLE-TOKEN-N7P4Q2R8
en la cabecera no se registra ni se indexa
Los tokens reciben vencimiento y vía de revocación. Todo token que haya viajado dentro de una URL se trata como comprometido y se invalida.
Con ese acceso, un atacante podía añadir, editar o eliminar usuarios del sistema; consultar la analítica y los datos de negocio sensibles; insertar datos maliciosos o fraudulentos; y leer los registros de actividad de los usuarios. No hacía falta ninguna técnica de explotación ni ninguna herramienta: bastaba con abrir una dirección que ya estaba publicada.
La causa raíz son dos decisiones, y las dos se corrigen: pasar el token como parámetro GET y no ponerle caducidad. Lo que se recomendó fue sacar el token de la dirección y llevarlo a la cabecera de autorización de la petición, donde no se registra ni se indexa; dar a los tokens un vencimiento y una vía de revocación; y, dado que una copia archivada por un tercero no se puede retirar a voluntad, tratar como comprometido e invalidar todo token que ya haya viajado dentro de una URL.