← Volver a todas las publicaciones
Publicado el · por Renaud Deraison

La vista previa podía leer tu carpeta personal

El 14 de octubre, Noma Labs reveló SneakPeek, un fallo en el panel de vista previa de Claude Code Desktop. Un archivo HTML inflado por encima de 256 KB y disfrazado de informe de cobertura de tests se cargaba desde el disco sin política de scripts, y su JavaScript leía claves SSH, secretos de AWS, kubeconfigs y los propios tokens OAuth de Claude en la carpeta personal del desarrollador, para luego enviarlos fuera con una baliza en segundo plano. Anthropic le dio un CVSS 8.7 y lo corrigió en agosto. En Bromure Agentic Coding, las páginas se renderizan en una VM aparte y desechable que no tiene ninguno de tus archivos, y la carpeta personal del agente guarda sustitutos en lugar de claves.

El repositorio parecía normal. El archivo era un informe de cobertura. El desarrollador le pidió al agente que lo mirara, y el panel de vista previa hizo el resto.

Un desarrollador clona un repositorio y le pide a Claude Code que mire el informe de cobertura de tests que contiene. Es una petición rutinaria. El informe es un archivo HTML. Cuando Claude lee o edita un archivo HTML en Claude Code Desktop, un panel de vista previa lo renderiza junto a la conversación, sin preguntar. Este informe lleva un script. En segundos, el script ha leído la clave privada SSH del desarrollador, sus claves de AWS, sus tokens de npm y GitHub, sus credenciales de Kubernetes y los propios tokens OAuth de Claude, y lo ha enviado todo a un servidor que controla el atacante.

Esa es la prueba de concepto que Noma Labs publicó el miércoles 14 de octubre con el nombre de SneakPeek. CSO Online lo cubrió ese mismo día. Anthropic puntuó el fallo con CVSS 8.7 (alto) y consideró su impacto equivalente a ejecución de código en el host. Noma lo había reportado el 4 de julio. La corrección llegó por actualización automática el 17 de agosto, en Claude Desktop 1.30096.1, después de que un primer parche fuera eludido. Un aviso de GitHub y un CVE están pendientes.

El bug está corregido. El escenario que hacía que valiera la pena encontrarlo sigue en la mayoría de los portátiles de desarrolladores: algo que renderiza contenido no confiable, ejecutándose como tú, junto a todo lo que tienes.

Tres defensas y un límite de tamaño

El panel de vista previa es un panel de Chromium dentro de la aplicación Electron. Noma contó tres defensas a su alrededor, y encontró un hueco en cada una.

Una política de scripts para archivos pequeños. Los archivos de menos de 256 KB se cargaban como URL data: en base64 con una Content Security Policy, una lista de reglas que le dice al navegador qué scripts pueden ejecutarse. Esa política bloqueaba los scripts. Los archivos de más de 256 KB se escribían en un archivo temporal y se cargaban desde el disco como URL file://, y la política no se aplicaba a ese camino. Inflar un informe por encima de 256 KB es trivial. Los informes de cobertura reales suelen tener ese tamaño.

Un origen con vía libre por el disco. El origen de una página decide qué puede leer. Una URL data: recibe un origen opaco que no puede leer nada. Una página cargada desde file:// obtenía, en palabras de Noma, “acceso de mismo origen a todo el sistema de archivos local de la víctima”. El script deducía la carpeta personal a partir de la ruta del archivo temporal y leía archivos con fetch() y XMLHttpRequest.

Un filtro que no estaba activado. El panel tenía un filtro de peticiones pensado para bloquear el acceso file://. Se activaba para los servidores de desarrollo que pasaban por el proxy y se quedaba apagado para el HTML estático renderizado desde el disco.

Para sacar los datos, el script llamaba a navigator.sendBeacon(), una API web que lanza en segundo plano un HTTP POST de un solo sentido. Nada en la aplicación limitaba adónde podía enviar peticiones el panel. El informe de Noma añade la dirección IP real del desarrollador, obtenida mediante WebRTC.

El primer parche de Anthropic, el 24 de julio, bloqueaba fetch() y XMLHttpRequest hacia rutas file:// desde la página. Ese mismo día, Noma trasladó las lecturas a un Web Worker iniciado desde una URL blob:, un contexto que el parche no cubría, y los tres defectos volvieron a funcionar. La corrección final de agosto cerró a la vez el hueco de la política, la carga file:// y el camino del worker. El investigador de Noma Eli Ainhorn lo resumió para CSO: “Las protecciones eran reales, simplemente no cubrían todos los caminos”.

En un portátil: mirar un informe, una baliza fueraRepositorio públicosrc/ tests/coverage/index.htmlinflado a +256 KBun script oculto“Mira el informede cobertura.”Panel de vista previa, como tú en el Macmenos de 256 KB: URL data:, sin scriptsmás de 256 KB: URL file://, sin política,mismo origen que todo tu discofetch ~/.ssh/id_ed25519fetch ~/.aws/credentialsfetch ~/.kube/configfetch ~/.docker/config.jsonfetch tokens OAuth de Claudesin pedir permiso, sin clicServidor del atacantesendBeacon POST-----BEGIN OPENSSHaws_secret_access…client-key-data:…tokens de npm y GitHuby tu IP real,vía WebRTCEl agente no ejecutó ningún comando. Leyó el renderizador, con el acceso de tu cuenta a tus archivos.
SneakPeek en un portátil. Un informe de cobertura inflado por encima de 256 KB se salta la política de scripts, se carga desde el disco con acceso a toda la carpeta personal, lee las claves del desarrollador y las envía con una baliza. Lo único que hizo el desarrollador fue pedirle al agente que mirara un archivo.

No fue el agente

SneakPeek no usa inyección de prompts. El agente leyó un archivo que le pediste leer, y la aplicación que lo rodea renderizó ese archivo. El script se ejecutó en el proceso de renderizado de la vista previa, con el acceso de la aplicación a tu disco.

Crystal Morin, de Sysdig, dijo a CSO que los registros a nivel de agente podrían no capturar esta ejecución, de modo que los defensores tendrían que vigilar las lecturas de archivos y las conexiones salientes en la capa de runtime. La transcripción de tu sesión mostraría una petición inofensiva y una respuesta inofensiva.

Un modelo cuidadoso, reglas de permisos estrictas y una persona aprobando cada comando no habrían cambiado nada. El renderizador estaba en el Mac, ejecutándose como tú, junto a tu carpeta personal y a todas las credenciales que has ido acumulando allí.

El consejo de Ainhorn a CSO apunta a esa segunda parte: mantener las credenciales de larga duración fuera de las máquinas de los desarrolladores y usar tokens de alcance limitado y corta duración.

El mismo informe en una máquina Bromure

En Bromure Agentic Coding, cada agente se ejecuta dentro de su propia VM Linux en tu Mac, llamada máquina, y el navegador integrado que renderiza páginas web se ejecuta en una segunda VM desechable a su lado. Clona el repositorio de Noma en una máquina y pide a Claude Code que mire el informe de cobertura.

Leer el informe es leer texto

El agente lee coverage/index.html como lee cualquier archivo: como texto en su contexto. Nada en el chat lo renderiza. Si haces clic en el archivo en el panel Files, la vista de detalle muestra el HTML como código fuente resaltado, igual que muestra un .py o un .json. No se ejecuta ningún script de la página. Los archivos de más de 512 KB no tienen vista previa. Un informe inflado por encima de 256 KB para esquivar una política de scripts aparece como 300 KB de marcado.

El renderizado ocurre en otra máquina

Cuando sí quieres ver el informe renderizado, el agente sirve la carpeta desde su máquina y la abre en el panel del navegador, en la dirección IP de la máquina. El navegador es un Chromium completo y se ejecuta en su propia VM auxiliar. Como dice el manual, “un archivo que el agente escribe en su directorio personal no está en el disco del navegador”.

El script se ejecuta y no encuentra nada que leer. El origen de la página es http://192.168.64.x:8000, un origen web corriente. Chromium no le deja leer rutas file://. Un script que lograra saltarse eso, con un worker o un bug del navegador, aterrizaría en un clon efímero copy-on-write de la imagen del navegador: sin ~/.ssh, sin ~/.aws, sin kubeconfig, sin token OAuth, sin ninguno de tus proyectos. La carpeta personal que deduce de una ruta temporal pertenece a una VM de Chromium vacía. La baliza sale vacía.

Por defecto, el navegador no guarda perfil. Stay signed in to websites está desactivado, así que las cookies y las sesiones desaparecen al cerrar el navegador, y un navegador oculto se destruye a los cinco minutos. WebRTC sigue apagado a menos que permitas la cámara o el micrófono en los ajustes Browser de la máquina, así que el truco de la dirección IP no tiene nada a lo que llamar.

La carpeta del agente guarda sustitutos

Ahora apunta el mismo script a la propia VM del agente. Supón que el comando de tests del repositorio escribe y abre el informe por sí mismo, de modo que el código se ejecuta donde trabaja el agente. Recorre la lista de Noma en esa carpeta personal:

  • Clave privada SSH. No hay ninguna. SSH_AUTH_SOCK se apoya en un puente de agente hacia el Mac. La VM puede listar identidades y pedir firmas, y los bytes de la clave privada nunca salen del host. ls ~/.ssh/id_* no encuentra nada.
  • AWS. ~/.aws/config apunta a un helper que entrega el ID de clave de acceso real con un secreto falso de 40 caracteres. El Mac vuelve a firmar cada petición con el secreto real de camino a AWS.
  • Kubernetes. ~/.kube/config es sintético. kubectl habla con el proxy del Mac, que guarda las credenciales reales del clúster.
  • Tokens de Docker, git y npm. ~/.docker/config.json y ~/.git-credentials contienen sustitutos con la forma de los reales. Los tokens que importas, desde un .env o a mano, se convierten en sustitutos limitados a sus hosts.
  • Los tokens OAuth de Claude. Se quedan en el Mac. El agente funciona con una clave de relleno, y el proxy pone el token de acceso real en la conexión para las peticiones a Anthropic.

Los sustitutos también son trampas. El proxy del Mac revisa cada petición que sale de la máquina, cabeceras y cuerpo, en busca de un sustituto que vaya hacia un host para el que no fue emitido. Una baliza que lleve la contraseña brm-docker-… o el token falso de GitHub al servidor del atacante recibe un HTTP 451. No se reenvía nada, la VM se pausa, y en el Mac se abre una alerta titulada This environment may have been compromised con una vista previa del token, el nombre de la credencial, el host al que pertenece y el host al que se envió. La detección queda como una fila roja en el Security Timeline. Configura Unmatched traffic en Deny en el firewall Outbound connections de la máquina, y una conexión a un host que el proyecto no usa se rechaza antes de todo eso.

Eso cubre el punto de Morin sobre la capa de runtime. Las comprobaciones están en el Mac, en la conexión, fuera del agente y fuera de cualquier cosa que se renderice dentro de la VM.

En Bromure: el renderizador sin archivos, el agente sin clavesVM de la máquina (agente)index.html leído como textopanel Files: código resaltadoCarpeta personal~/.ssh/id_* (ninguna)~/.aws helper, secreto falso~/.kube/config sintéticodocker, git sustitutos brm-…clave Claude de rellenosirve coverage/ en :8000ningún secreto real que leerVM del navegador (desechable)http://192.168.64.x:8000el script correlecturas file:// rechazadasdisco: clon nuevo, sin tus archivossin perfil, WebRTC apagadosendBeacon(…) → vacíose destruye 5 minutostras ocultarseEl Macel proxy pone la clave realsolo para su hostsustituto a host erróneo:451, VM pausada, alertafirewall: host desconocido negadoSecurity Timeline:fila roja, ambos hostsEn el Mac, fuera de ambas VMClaves privadas SSH · secretos AWS · credenciales de clúster · tokens de registro y git · tokens OAuth de Claude · Security Timeline
El mismo informe en Bromure. El agente lo lee como texto y el panel Files lo muestra como código fuente. Cuando se renderiza, lo hace en una VM de navegador desechable que no tiene ninguno de tus archivos. La carpeta personal del agente guarda sustitutos, y un sustituto enviado al host equivocado detiene la máquina.

La corrección y la próxima vista previa

Anthropic corrigió SneakPeek en agosto y Noma volvió a probarlo. Pasaron seis semanas entre el reporte y la corrección, y el primer parche cayó ante un Web Worker el mismo día en que salió. Las correcciones de renderizadores suelen ir así. Un motor de navegador tiene muchas formas de iniciar un script y muchas formas de hacer una petición, y cada defensa tiene que cubrirlas todas.

Las herramientas de programación siguen añadiendo paneles que renderizan cosas: vistas previas de HTML, Markdown con contenido incrustado, salidas de notebooks, servidores de desarrollo, capturas de la aplicación que el agente acaba de construir. Cada uno es un renderizador apuntado a contenido que el desarrollador no escribió, de repositorios clonados hace un minuto. En un portátil, cada uno se ejecuta junto a la carpeta personal.

En Bromure, el panel que renderiza páginas se ejecuta en una VM que no tiene ninguno de tus archivos, y la máquina donde trabaja el agente guarda sustitutos. Un bug del renderizador ahí expone un Chromium desechable y unas cadenas que ningún servidor aceptará.

Código fuente, no una página

El agente lee el HTML como texto, y el panel Files lo muestra como código fuente resaltado. Abrir un informe para revisarlo no ejecuta ninguno de sus scripts.

Un navegador sin nada en el disco

Las páginas se renderizan en una VM de Chromium aparte y desechable. Los archivos del agente y los de tu Mac no están en su disco, y lo olvida todo al cerrarse.

Una carpeta personal llena de sustitutos

Ninguna clave privada SSH, un secreto de AWS falso, un kubeconfig sintético, sustitutos de tokens de registro y git. Las credenciales reales se quedan cifradas en el Mac.

Una alarma en la conexión

Un sustituto enviado a un host para el que no fue emitido recibe un 451. La VM se pausa, una alerta nombra ambos hosts y el Security Timeline guarda el registro.

Mira el informe

“Mira el informe de cobertura” es de las peticiones más corrientes que se le pueden hacer a un agente de programación. Los desarrolladores seguirán preguntando por archivos de repositorios que no escribieron, y las herramientas seguirán renderizando esos archivos para ser útiles.

Pregúntalo en una máquina Bromure, donde la página se renderiza en una VM sin nada que leer y las claves del agente son sustitutos. Para saber más sobre los sustitutos y la alarma que disparan, lee El agente filtró su propia clave y La clave era tu archivo .env.