Reports
BAJA[EXPOSICIÓN DE DATOS]#4071670

HackerOne Code enviaba a Datadog RUM tokens de restablecimiento de contraseña vigentes junto a la cuenta afectada

Al abrir un enlace de restablecimiento en app.pullrequest.com, Datadog RUM guardaba la URL completa con el token aún sin usar, y la misma sesión registraba el usuario de la cuenta. Faltaba beforeSend en la configuración.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

HackerOne Code (app.pullrequest.com) envía el token de restablecimiento de contraseña dentro de la URL del enlace que llega por correo, como clave suelta de la query string. Esa página carga el SDK de navegador de Datadog RUM (versión 4.14.0), que anota la URL entera en el campo view.url de cada vista y la manda a rum.browser-intake-datadoghq.com. Resultado: el token de 64 caracteres hexadecimales, todavía vigente porque el formulario aún no se ha enviado, llega a Datadog en cuanto se abre el enlace.

El origen es la configuración del SDK. Al iniciarlo no se define ningún callback beforeSend, que es el punto que Datadog ofrece para limpiar campos antes de enviarlos, y sampleRate vale 100. Por tanto, se registran todas las sesiones de todos los usuarios y no se redacta ninguna URL en ninguna ruta.

Además, la misma sesión de RUM (session_id) recoge más tarde las rutas autenticadas, que llevan el usuario (handle) de la cuenta en la ruta. El endpoint de restablecimiento pide token y email a la vez; con el token y el handle agrupados bajo una misma sesión, quien lea esos datos tiene mucho más fácil saber a qué cuenta pertenece cada token.

El investigador lo relaciona con un reporte anterior, el #4000185, sobre el mismo activo: entonces el token se filtraba a Segment a través de los eventos de página automáticos. Aquella corrección añadió un middleware de Segment (analytics.addSourceMiddleware) que limpia la URL solo en las rutas /password-reset y /forgot-password, y solo dentro del flujo de Segment. Datadog RUM es otra librería, con su propio envío, así que ese arreglo no le afectaba. Durante las pruebas, la página ya no mandaba nada a Segment, Intercom ni Sentry: Datadog era el único destino que recibía la URL con el token.

| Elemento | Valor | | --- | --- | | Página | app.pullrequest.com/password-reset | | Endpoint de restablecimiento | POST /api/v1/password-reset (campos token, email, password) | | SDK | Datadog RUM browser SDK 4.14.0 | | Destino | rum.browser-intake-datadoghq.com/api/v2/rum | | Etiquetas | service:pullrequest, env:production |

Configuración leída desde la consola del navegador:

> Object.keys(window.DD_RUM.getInitConfiguration())
["applicationId","clientToken","site","service","env","version","sampleRate","trackInteractions"]

> typeof window.DD_RUM.getInitConfiguration().beforeSend
"undefined"

> window.DD_RUM.getInitConfiguration().sampleRate
100

Forma del enlace de restablecimiento (el token es la clave, sin valor, antes de los parámetros de campaña):

https://app.pullrequest.com/password-reset?<64 hex characters>&utm_swu=547&utm_source=account%20action%20emails&utm_campaign=Reset%20Password%20...&utm_medium=email

Pasos de reproducción

  1. Pide un restablecimiento de contraseña para una cuenta tuya en app.pullrequest.com.
  2. Abre el enlace del correo, pero no envíes el formulario. La página muestra con normalidad los campos de email y nueva contraseña, así que el token sigue sin usar.
  3. En las herramientas de red verás peticiones POST a Datadog con service:pullrequest, aceptadas con HTTP 202 y repetidas cada unos 30 segundos mientras la vista sigue activa:
POST https://rum.browser-intake-datadoghq.com/api/v2/rum
     ?ddsource=browser&ddtags=sdk_version:4.14.0,env:production,service:pullrequest...
     &batch_time=1790747851585
-> HTTP 202

POST https://rum.browser-intake-datadoghq.com/api/v2/rum
     ...&batch_time=1790747881586
-> HTTP 202
  1. En la consola, con TOKEN igual a los 64 caracteres hexadecimales del enlace, comprueba que el token está en el campo que se envía y cómo está configurado el SDK (anota el session_id):
window.DD_RUM.getInternalContext().view.url.includes(TOKEN)   // true
typeof window.DD_RUM.getInitConfiguration().beforeSend        // "undefined"
window.DD_RUM.getInitConfiguration().sampleRate               // 100
window.DD_RUM.getInternalContext().session_id                 // note this
  1. Completa el restablecimiento e inicia sesión. En una página autenticada vuelve a leer session_id: es el mismo, y el view.url de esa vista contiene el handle de la cuenta. El investigador lo resume así:
view 1   /password-reset?<live token>        session_id  [REDACTADO]...
view 2   /dash/scans/1/<account handle>      session_id  [REDACTADO]...
  1. Para comprobar que la captura no depende de la página de restablecimiento, en cualquier ruta autenticada se puede añadir un parámetro inventado con un marcador sin valor real. RUM también lo registra, sin cambiar de sesión:
> history.pushState({}, '', location.pathname + '?demo_param=' + MARK);
rum_tracked_the_spa_route_change      true
RUM_CAPTURED_ARBITRARY_QUERY_VALUE    true
session_id                            unchanged
  1. Desde el lado del programa, con acceso a Datadog, se confirma con esta consulta en RUM Explorer:
service:pullrequest   @view.url:*password-reset*

El investigador comprobó también que el token es de un solo uso: enviar uno ya consumido a POST /api/v1/password-reset devuelve HTTP 422. Un token seguía siendo válido unos 15 minutos después de emitirse; es un mínimo medido, no la caducidad real, que no llegó a averiguar.

Impacto

Cualquiera que pudiera leer los datos de RUM de service:pullrequest en Datadog obtenía tokens de recuperación vigentes y, en la misma sesión, la cuenta a la que pertenecían. Eso incluye a quien tenga acceso de lectura a RUM en esa organización de Datadog, las claves de API o integraciones con permiso para leerlo, y cualquiera que llegara a conseguir esos accesos. Ese grupo no está controlado por el modelo de permisos de la aplicación. El token no necesita contraseña ni segundo factor para usarse.

El caso más realista es el restablecimiento abandonado: el usuario abre el enlace y no termina (le interrumpen, lo abre en otro dispositivo o recuerda la contraseña). El token queda válido en la telemetría de un tercero hasta que caduca, sin que el usuario lo sepa. Según el investigador, Datadog conserva por defecto los eventos de RUM 30 días.

Al faltar beforeSend por completo, cualquier secreto que acabe en una URL de la aplicación se registraría igual, en cualquier ruta.

Límites que el propio investigador señala: no afecta a credenciales de sesión, porque las peticiones autenticadas llevan el bearer token en una cabecera y no en la URL. Tampoco encontró códigos de autorización OAuth en URLs capturadas, ya que /oauth/callback responde con un 302 del servidor y no llega a ejecutarse script en esa página. Sin acceso al Datadog del programa, solo pudo demostrar la parte del cliente.

El investigador pidió severidad Media (CVSS 3.1 AV:N/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:N, 5.9), pero el reporte consta como Baja, igual que el #4000185. Hubo recompensa, de importe no público.

Remediación

El reporte figura como resuelto, pero no detalla qué cambio se aplicó. Lo que propuso el investigador:

  1. Sacar el token de la URL: aceptarlo en el cuerpo del POST, o canjearlo en el servidor en la primera carga por una cookie de vida corta y redirigir a una ruta limpia. Así se cubren de una vez RUM, analítica, Referer, historial del navegador y logs de CDN o proxy.
  2. Mientras tanto, definir beforeSend en el SDK de RUM para quitar la query string, eliminándola por defecto y dejando pasar solo los parámetros que se sepa que son seguros.
  3. Borrar los eventos guardados que coincidan con @view.url:*password-reset* y dar por expuesto cualquier token que siga vigente.
  4. Valorar una caducidad más corta para los tokens de restablecimiento.
  5. Poner una Referrer-Policy restrictiva explícita en las páginas que llevan credenciales.

Qué aprender de este caso

  • Si corriges una fuga de un secreto a un proveedor de telemetría, revisa todos los SDK que carga la página (Segment, Datadog, Sentry, Intercom…): cada uno envía por su cuenta y tiene su propio hook de limpieza, y arreglar uno no arregla los demás.
  • Al auditar una aplicación con Datadog RUM, ejecuta window.DD_RUM.getInitConfiguration() en la consola: si no hay beforeSend y sampleRate es 100, cualquier parámetro sensible en la URL acaba en Datadog para todos los usuarios.
  • Busca tokens de un solo uso que viajen en la query string (restablecimiento, invitaciones, confirmación de email) y comprueba si session_id u otro identificador del mismo proveedor los relaciona después con rutas que revelan la cuenta.
  • Para evitarlo de raíz, no metas secretos en la URL: canjéalos en el servidor por una cookie de vida corta y redirige a una ruta limpia antes de que se cargue ningún script de terceros.