Todos los artículos
Incidente6 min de lectura

Un script de un proveedor vació 2,5 millones de dólares de Polymarket. Audita los tuyos hoy

Browser window with a row of puzzle-piece plugins, one glowing orange piece pulling data streams out of a safe

El jueves pasado, 29 de octubre, Polymarket puso su plataforma en modo de solo cancelación. Los usuarios informaban de cuentas vaciadas y, en cuestión de una hora, la empresa confirmó la causa. En sus propias palabras, "uno de nuestros proveedores externos fue comprometido", según recoge The Block.

Ese proveedor, según The Wall Street Journal a través de Finance Magnates, era DebugBear, un servicio de monitorización del rendimiento cuyo código se ejecutaba en la web de Polymarket. Un análisis de la blockchain citado en ese artículo rastreó más de 2,5 millones de dólares movidos en más de 900 transacciones hacia una wallet vinculada al atacante. Polymarket afirma que compensará a los usuarios afectados.

No se filtró ninguna contraseña. No hubo phishing de ninguna seed phrase. Los usuarios simplemente tenían la sesión iniciada en un sitio que cargaba un archivo JavaScript desde el servidor de otra empresa, y ese archivo cambió.

Si tu app carga analítica, widgets de chat, tests A/B, grabación de sesiones o monitorización del rendimiento desde un CDN de terceros, estás confiando a esos proveedores todo lo que puede hacer tu propio código. Esto es lo que pasó, por qué te afecta aunque no gestiones un exchange y cómo revisar tu app esta misma semana.

Qué pasó

  • La web de Polymarket cargaba un script de monitorización de usuarios reales desde el CDN de DebugBear. Es una configuración normal. Miles de sitios hacen lo mismo con decenas de proveedores.
  • El atacante sustituyó ese archivo por JavaScript malicioso. Como el navegador lo descargaba directamente del proveedor en cada visita, todos los visitantes recibieron la nueva versión al instante.
  • El script comprobaba si había una sesión activa de Polymarket, leía del almacenamiento del navegador los datos de la cuenta, los detalles de la wallet y las credenciales de la API de trading, y después usaba el propio flujo de firma de Polymarket para mover posiciones o vender activos hacia una wallet del atacante.

El detalle que conviene pensar con calma: los investigadores señalaron que ni las hardware wallets ni las contraseñas fuertes sirvieron de nada, porque el script nunca necesitó una clave privada. Actuaba como el usuario con la sesión iniciada, desde dentro de su propia sesión en el navegador.

La solución de Polymarket, según el mismo informe, fue un commit en producción que eliminó el script del proveedor. El trading se reanudó esa misma tarde. SafeState también señala que no era el primer incidente con scripts de proveedores que sufría la plataforma este año.

Por qué esto importa para tu app

Una etiqueta script que apunta a cdn.some-vendor.com se ejecuta con el mismo poder que tu propio código. Puede leer el DOM, leer localStorage y sessionStorage, llamar a tu API con las credenciales del usuario y cambiar lo que muestra la página. No hay ningún sandbox entre "tu código" y "el snippet de analítica que pegaste la semana del lanzamiento".

Esto es especialmente relevante en apps lanzadas con prisa y en apps hechas con IA, porque:

  • Los scripts se acumulan. Cada prompt de "añade analítica", "añade una burbuja de chat" o "añade seguimiento de errores" suele sumar otra etiqueta. Poca gente las quita después.
  • Los tokens viven en el almacenamiento del navegador. Muchas plantillas de inicio guardan el token de sesión o de API en localStorage, donde cualquier script de la página puede leerlo.
  • Normalmente no hay Content-Security-Policy. Sin ella, cualquier script inyectado o sustituido puede cargar más código desde cualquier sitio y enviar datos a cualquier parte.

No puedes auditar los pipelines de build de tus proveedores. Pero sí puedes limitar el daño que puede hacer un script malicioso.

Cómo revisar tu app

1. Haz una lista de todos los scripts que cargan tus páginas. Abre tu app, luego DevTools, la pestaña Network, filtra por JS y recarga. O ejecuta esto en la consola en cada página importante:

[...document.scripts].map(s => s.src || '(inline)').filter(Boolean)

Apunta cada dominio que no sea tuyo. Hazlo también en las páginas con sesión iniciada, no solo en la web de marketing. Los gestores de etiquetas pueden cargar más scripts que no aparecen en el código fuente HTML.

2. Comprueba cuáles de esas etiquetas tienen atributo integrity. En la consola:

[...document.querySelectorAll('script[src]')]
  .filter(s => !s.src.startsWith(location.origin))
  .map(s => ({ src: s.src, sri: !!s.integrity }))

3. Revisa tus cabeceras de respuesta. Ejecuta curl -sI https://yourapp.com y busca Content-Security-Policy. Si no está, o contiene script-src * o 'unsafe-inline' sin nonces, no está limitando gran cosa.

4. Comprueba dónde vive tu sesión. En DevTools, mira en Application las secciones Local Storage y Session Storage. Si ves ahí un access token, un refresh token o una API key, cualquier script de la página puede leerlo, que es justo lo que hizo el payload de Polymarket.

Cómo solucionarlo

Quita lo que no necesites. Es el paso que más valor aporta. Las recomendaciones de SafeState para los operadores empiezan por mantener un inventario de scripts de terceros y eliminar las etiquetas no esenciales de las páginas de cuenta, pago y transacciones. Tus páginas de login, facturación y ajustes rara vez necesitan una herramienta de mapas de calor.

Fija y verifica lo que conserves con Subresource Integrity (SRI). Si un proveedor ofrece archivos versionados, referencia una versión concreta y añade un hash. Si el archivo cambia, el navegador se niega a ejecutarlo:

<script
  src="https://cdn.example-vendor.com/sdk/4.2.1/sdk.min.js"
  integrity="sha384-REPLACE_WITH_REAL_HASH"
  crossorigin="anonymous"></script>

Genera tú mismo el hash a partir del archivo exacto que revisaste:

curl -s https://cdn.example-vendor.com/sdk/4.2.1/sdk.min.js \
  | openssl dgst -sha384 -binary | openssl base64 -A

Ten en cuenta la contrapartida: SRI rompe los scripts que los proveedores actualizan in situ en una URL sin versión, algo habitual en snippets de monitorización y analítica. Para esos, o bien alojas tú una copia fijada que actualizas de forma deliberada, o bien aceptas que confías en el archivo en vivo del proveedor y lo mantienes fuera de las páginas sensibles.

Añade una Content-Security-Policy. CSP le dice al navegador qué orígenes pueden servir scripts y a qué orígenes puede enviar datos la página. Un punto de partida para una app de Next.js o Express podría ser este:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example-vendor.com;
  connect-src 'self' https://api.yourapp.com;
  frame-ancestors 'none';
  base-uri 'self';
  object-src 'none'

Sé realista sobre lo que te aporta CSP aquí: si el dominio del propio proveedor está permitido, CSP no detendrá un archivo malicioso servido desde él. Lo que hace CSP es impedir que ese script cargue código desde dominios nuevos o envíe datos robados a endpoints arbitrarios que no estén en connect-src. Despliégala primero con Content-Security-Policy-Report-Only para ver qué se rompe. Nuestra guía sobre cómo añadir una Content-Security-Policy lo explica paso a paso.

Content-Security-Policy: la guía de configuración completa

Saca los tokens de sesión del alcance de JavaScript. Mueve los tokens de sesión a cookies marcadas como HttpOnly, Secure y SameSite, para que los scripts de la página no puedan leerlos. No impedirá que un script malicioso haga peticiones como el usuario mientras la página está abierta, pero evita que los tokens se roben y se reutilicen en otro sitio. Consulta los flags de cookies Secure, HttpOnly y SameSite.

Mantén limpias tus propias claves de frontend. Si un script puede leer tu bundle, puede leer cualquier secreto que hayas incluido en él, y los bots ya están buscando claves filtradas y archivos .env. Revisa nuestra guía sobre API keys expuestas en JavaScript del lado del cliente, asegúrate de no publicar source maps en producción y, si ya te has llevado un susto, nuestro artículo sobre cómo rotar los secretos de tu app sin caídas explica la limpieza.

Secure, HttpOnly y SameSite: protege tus cookies de sesión

Checklist final

Antes de cerrar DevTools, asegúrate de que:

  • Tienes una lista de todos los scripts de terceros de tus páginas públicas y con sesión iniciada.
  • Has quitado los scripts que ya no usas, sobre todo de las páginas de login, facturación y cuenta.
  • Has fijado versiones y añadido hashes de integridad donde los proveedores lo permiten.
  • Los scripts que no se pueden fijar están autoalojados o fuera de las páginas sensibles.
  • Publicas una Content-Security-Policy con script-src y connect-src estrictos.
  • Los tokens de sesión están en cookies HttpOnly, no en localStorage.
  • Repites esta auditoría cada vez que alguien (o algún agente de IA) añade una etiqueta nueva.

Descubre en qué punto estás

Si quieres una referencia rápida, AppSafe revisa tus cabeceras de seguridad, incluido si hay una Content-Security-Policy configurada, y su escaneo completo verificado analiza problemas de frontend como Subresource Integrity y librerías de JavaScript vulnerables. Puedes ver cómo es un informe en el informe de ejemplo, o combinar esto con nuestra checklist previa al lanzamiento para apps hechas con IA.

Polymarket encontró la causa en aproximadamente una hora tras detener el trading. Muchos equipos pequeños tardarían mucho más en notar que un script ha cambiado. Ejecuta un escaneo gratuito de AppSafe en tu app hoy y descubre en qué punto estás.

Ejecuta este checklist automáticamente

Un solo escaneo gratis revisa todos estos problemas en tu app y te da un prompt de corrección con IA para cada hallazgo.

Escanear mi app gratis