Jorge Turbica / blog · Blog ← Todos los artículos EN · Read in English
IA

Un enlace compartido de ChatGPT bastaba para robar tus datos: cuatro fallos en cadena

Un postMessage sin control, un innerHTML y un dominio permitido en la CSP bastaron para tomar cuentas de ChatGPT. Lecciones si muestras HTML ajeno.

Abrir un enlace compartido de ChatGPT con la sesión iniciada era suficiente para que un atacante leyera tus datos privados o actuara en tu nombre. Ninguno de los cuatro fallos que lo hacían posible era grave por sí solo; juntos daban control completo de la cuenta. Lo publicó Parameter Labs y lo resumió Simon Willison el 31 de octubre de 2026. OpenAI ya lo ha corregido y pagó 5.000 dólares de recompensa.

El interés va más allá de ChatGPT: cualquier producto que muestre HTML generado por un modelo o por el usuario (artefactos, previsualizaciones, widgets, dashboards embebidos) tiene la misma superficie.

El punto de partida: HTML ajeno en un iframe sandbox

ChatGPT muestra apps HTML interactivas, lo que OpenAI llamó primero "Canvas" y ahora "HTML previews", dentro de un iframe con sandbox. La premisa de seguridad es sencilla: ese código no es de confianza y no debe poder hablar con las APIs de OpenAI ni acceder a datos privados. Todo el ataque consiste en romper esa frontera.

La cadena, paso a paso

  1. Un canal abierto hacia la página principal. ChatGPT usa la librería MathLive para sus gráficos, y su teclado virtual se puede activar desde un iframe con postMessage(). Nada impedía que otros frames con sandbox, incluidos los de las previsualizaciones HTML, enviaran ese mensaje.
  2. Un innerHTML en el receptor. El código que recibía el mensaje insertaba su contenido con innerHTML, así que cualquiera que pudiera enviar el mensaje podía añadir HTML a la página principal.
  3. Un iframe nuevo, sin sandbox. innerHTML no ejecuta etiquetas <script>, pero sí permite crear un iframe nuevo sin las restricciones del sandbox.
  4. Un dominio de confianza que devuelve lo que le pidas. La CSP de ChatGPT bloqueaba los scripts inline y solo permitía cargar scripts de unos pocos dominios, YouTube entre ellos. El endpoint de oEmbed de YouTube se podía usar para que devolviera JavaScript y el navegador lo ejecutara.

Con JavaScript corriendo en el origen de ChatGPT, leer datos privados de las APIs y sacarlos con una referencia a una imagen era trivial. El resultado final era un enlace compartido de ChatGPT que, abierto por otro usuario con sesión iniciada, exfiltraba sus datos.

Qué se puede aprender

Un sandbox protege la página de un frame, no a los frames entre sí. El fallo central no está en el iframe de la previsualización, sino en un receptor de mensajes de la página principal que daba por hecho que solo le hablaría el frame previsto. Cada addEventListener("message") es una API pública para cualquier frame de la página y hay que tratarlo como tal: comprobar event.source contra la ventana que se espera y no solo event.origin, porque los iframes con sandbox sin allow-same-origin tienen origen "null" y comparten ese valor con cualquier otro frame igual.

Los datos de un mensaje son texto, no HTML. Si un receptor necesita pintar algo, textContent o la construcción de nodos con la API del DOM evitan que un mensaje se convierta en marcado. Con innerHTML, la pregunta deja de ser "¿puede ejecutar scripts?" y pasa a ser "¿qué otros elementos puede crear?", y un iframe sin sandbox es la respuesta.

Una lista de dominios en la CSP solo es tan segura como su endpoint más permisivo. Permitir un dominio entero para scripts incluye cualquier ruta de ese dominio que refleje contenido controlado por el atacante. Los nonces o hashes por script, y alojar los scripts de terceros en un origen propio, cierran esa puerta mejor que una lista de hosts.

El contenido no confiable va a otro origen. Los productos que muestran HTML generado suelen servirlo desde un dominio separado, sin cookies ni acceso a las APIs del producto principal, de modo que aunque alguien escape del sandbox no aterriza en el origen que importa. En este caso el escape acababa en el origen de ChatGPT, con la sesión del usuario.

Así queda el patrón vulnerable y una versión defensiva de un receptor de mensajes:

javascript
// Vulnerable: cualquier frame de la página puede inyectar marcado
window.addEventListener("message", (e) => {
  panel.innerHTML = e.data.html;
});

// Defensivo: solo la ventana esperada y solo texto
window.addEventListener("message", (e) => {
  if (e.source !== widgetFrame.contentWindow) return;
  if (typeof e.data?.label !== "string") return;
  panel.textContent = e.data.label;
});

Qué no es trasladable

El detalle del ataque depende de piezas concretas de ChatGPT: MathLive, su CSP y la presencia de YouTube en ella. Copiar la lista de comprobaciones de este caso no garantiza nada en otra aplicación; lo que sí se traslada es el método: enumerar todos los receptores de postMessage de la página, todos los dominios de la CSP y qué devuelve cada uno, y asumir que un atacante va a usar los tres a la vez.

La recompensa, 5.000 dólares, le pareció a Willison "un poco barata" para un fallo de este calibre. Para quien construya con agentes y artefactos, la cifra importa menos que la lección: la superficie de ataque de un producto de IA no es solo el modelo.

Fuentes

Jorge Turbica

Software Engineer y SRE. Escribo sobre los retos reales que me encuentro en producción.

Ver portfolio ↗