Volver a todas las publicaciones
Publicado el · por Renaud Deraison

Nadie publicó una versión nueva

El aviso de Wordfence del 9 de agosto sobre el compromiso de BdThemes describe un ataque a la cadena de suministro sin cadena de suministro. Ninguna publicación, ningún script de instalación, ningún typosquat, ningún archivo modificado en el repositorio del plugin. Los atacantes consiguieron acceso de escritura al bucket de almacenamiento del proveedor y cambiaron un feed JSON que 350.000 sitios descargaban en cada carga del panel de administración. Para quien programa, la pregunta que esto plantea es qué tiene permitido escribir la máquina donde corre tu agente de código, y ante quién.

Todos los controles de cadena de suministro que tienes inspeccionan cosas que se publican. Este no publicó nada, y aun así llegó a 350.000 sitios.

El 7 de agosto de 2026, Wordfence empezó a recibir avisos de sitios WordPress con plugins de BdThemes a los que les habían crecido cuentas de administrador que nadie creó. La lista llega a siete plugins: Element Pack Addons for Elementor, Prime Slider, Pixel Gallery, Ultimate Post Kit, Ultimate Store Kit, Live Copy Paste y Smart Admin Assistant. Element Pack por sí solo supera las 100.000 instalaciones activas, y los siete juntos pasan de 350.000.

Para el 8 de agosto el proveedor había limpiado su API y WordPress.org había retirado los siete plugins a la espera de la investigación. Wordfence publicó su PSA sobre el compromiso al día siguiente, y su hallazgo central es una negación: nadie había tocado el código de los plugins.

No se publicó nada

Los plugins de BdThemes llevan dentro un componente llamado Biggopti cuyo trabajo es modesto y aburrido: traer banners promocionales de la API remota del proveedor y dibujarlos en el panel de administración de WordPress. Los datos del banner son un archivo JSON estático en un bucket de DigitalOcean Spaces, servido por detrás de Cloudflare.

Los atacantes consiguieron acceso de escritura a ese bucket. Esa es toda la intrusión.

Biggopti sacaba el campo display_id de la respuesta JSON y lo dejaba caer en un atributo HTML id sin escaparlo, un fallo de cross-site scripting que Wordfence puntuó como CVSS 5.4, medio y rastreó hasta el 1 de marzo de 2026, cuando alguien lo escribió una vez y lo copió de un plugin al siguiente. Un display_id que lleva un manejador onanimationstart es, por tanto, JavaScript, y se ejecuta en el navegador de cada administrador con la sesión abierta, en cada carga de página de wp-admin, en cada sitio con el plugin instalado.

El payload aprovechó bien ese asiento. Un script llamado w2.js pedía instrucciones de selección de objetivos a un servidor de mando y control, la máquina que un atacante usa para decirle a un payload qué sitios merece la pena tomar, y luego creaba cuentas de administrador ocultas a través de la API REST de WordPress, a lomos de la sesión y los nonces del administrador de verdad. Una variante derivaba nombres de usuario predecibles del nombre de host del sitio. Un plugin falso, a menudo llamado algo así como wp-smart-thumbnails, entregaba un web shell en emer-run.php, es decir, un script que un atacante maneja cargando una URL corriente. Unos must-use plugins, que WordPress carga en cada petición y oculta de la pantalla de plugins, llevaban una puerta trasera de acceso mágico y un módulo que disimulaba las cuentas nuevas reescribiendo los totales de usuarios que muestra la pantalla de administración.

Las marcas de tiempo de los registros envenenados sitúan el inicio más temprano posible el 23 de junio de 2026, siete semanas antes de que nadie se diera cuenta.

Pon eso al lado de los controles que ejecuta un equipo cuidadoso. Fijación de versiones: las versiones nunca cambiaron. Lockfiles y hashes de integridad: coincidían, porque los archivos que cubrían estaban intactos. Revisión de scripts de instalación: no hubo instalación. Detección de typosquat: nombre correcto, proveedor correcto, entrada de repositorio correcta. Revisión de código del diff entre versiones: no hubo diff, porque no hubo versión. Certificado y CDN: los dos con salud perfecta. Cloudflare y TLS demostraron que el JSON llegaba al sitio sin modificar desde el bucket del que se suponía que venía, que es precisamente lo que lo hacía útil.

El camino que todo vigilaWordPress.orgsiete plugins, 350.000+instalaciones activaslo que ven los controlesversiones, hashes, scriptsde instalación, el diffveredicto: limpioy es correcto. Ningún archivo delrepositorio se modificó jamás.El camino que nadie vigilabael bucket del proveedorDigitalOcean Spaces,por detrás de Cloudflarealguien podía escribir ahíBiggopti trae el bannerdisplay_id entra en un atributoid de HTML sin escaparonanimationstart lo ejecutaen la sesión del propio admincuentas admin ocultas vía REST,web shell emer-run.php,magic-login en mu-pluginsprimer registro envenenado: 23 de junio de 2026Wordfence avisado: 7 de agosto de 2026Siete semanas en las que cada comprobación del artefacto publicado pasó, y pasó con razón.
Dos caminos hacia un sitio con el plugin instalado. El camino publicado, el que inspecciona cualquier control de cadena de suministro, no cambió nunca. El camino en ejecución traía un archivo JSON de un bucket en el que otra persona podía escribir, y ese archivo llegaba como JavaScript al navegador del administrador.

El incidente empieza con una credencial que podía escribir

Lee el aviso desde el lado del proveedor y la cadena de ataque se reduce a un paso. Alguien obtuvo acceso de escritura a un bucket de almacenamiento. Todo lo demás es consecuencia.

Los compromisos aguas arriba siguen llegando con esa forma. El gusano del scope de npm de Red Hat, el repositorio que sí era de Microsoft, los marketplaces de extensiones, los registros de contenedores: en todos ellos el payload es la parte que se cuenta, y la parte que importaba era que alguien acabó con una credencial capaz de sobrescribir un artefacto que otros descargan de forma periódica.

¿Dónde vive una credencial así? En el entorno de quien programa. Un DIGITALOCEAN_ACCESS_TOKEN en una shell, una configuración de doctl en ~/.config, un login de registro en ~/.docker/config.json, un perfil de AWS, un token de GitHub en ~/.git-credentials, una clave SSH que puede hacer push. En un portátil, en una terminal, al lado del proyecto. En 2026, al lado del proyecto es también donde corre tu agente de código.

La mayoría de los artículos de este blog describe una forma de volver ese agente contra ti. Un informe de error que ejecutó un comando. Una página web que reescribió la configuración del agente. Un comentario que nadie podía ver. Una habilidad que se ejecutó con tu identidad. Tres saltos de arnés en una sola charla de Black Hat. Da el catálogo por sabido y asume que un martes cualquiera uno de ellos te toca a ti.

Lo mal que salga ese martes depende de una pregunta que puedes responder hoy, sin saber qué inyección gana: ¿qué tiene permitido escribir esta máquina, y ante quién? En un portátil la respuesta es todo lo que tú tienes permitido escribir, y si publicas cualquier cosa, eso incluye el artefacto que varios cientos de miles de desconocidos descargan sin mirar.

El token no está en la máquina

Bromure Agentic Coding ejecuta el agente en una VM de Linux desechable sobre Apple Silicon, y cada byte de su tráfico cruza un proxy en el host, fuera de la caja en la que corre el agente. Bromure coloca las credenciales alrededor de esa frontera en lugar de meterlas dentro.

Pon tu token de acceso personal de DigitalOcean en el panel Credentials y Bromure guarda el valor real en el Mac. Lo que aterriza en la VM es un señuelo: una variable de entorno DIGITALOCEAN_ACCESS_TOKEN y un ~/.config/doctl/config.yaml que hacen funcionar doctl sin doctl auth init, con un marcador de posición dentro. Cuando la VM lanza una petición a api.digitalocean.com, el proxy del host sustituye el token real en el cable, para ese destino, y para nada más.

Cualquier cosa que corra dentro de esa VM puede leer el entorno, hacer grep en los dotfiles y recorrer el disco entero: el agente, una dependencia, un paso de compilación, un comando de shell salido de una incidencia envenenada. Lo que recoge es brm_…. El mismo arreglo cubre el resto del conjunto: tokens de GitHub, GitLab y Bitbucket, logins de registros de contenedores escritos como un blob base64 falso en ~/.docker/config.json, un kubeconfig sintético con certificados de cliente desechables, Linear, los endpoints HTTPS de bases de datos y cualquier otra cosa que añadas en Other API keys. AWS va más lejos: el host vuelve a firmar cada petición con SigV4 usando el secreto real, de modo que saltarse el proxy te gana una InvalidSignatureException en vez de una escritura no autorizada.

Guardrails decide el verbo, en el Mac

Este incidente necesitó una sola operación: sobrescribir un objeto en un bucket. Guardrails es un motor de políticas dentro del proxy del host, con un modo por recurso: Off, Block destructive o Read-only. Pon DigitalOcean en Read-only y cada mutación hacia api.digitalocean.com vuelve como un 403 seco que el agente reporta como un fallo de API corriente. Bromure toma esa decisión en macOS. No hay ningún ajuste dentro de la VM que cambiar, ninguna variable de entorno que desactivar y ningún archivo que editar.

Publicar pregunta antes

Cada entrada de credencial tiene Require approval to use. Actívala y cada intercambio señuelo→real levanta un diálogo de consentimiento en el host antes de que el valor real llegue al cable, con permisos acotados en el tiempo en la ruta SSH: cinco minutos, una hora, el resto de la sesión. Publicar una versión es algo que querías hacer, y te cuesta un clic. Una escritura que tú no empezaste aparece como un diálogo que no esperabas, en un momento en el que no esperabas ninguno.

Registros y git reciben el mismo trato

Guardrails clasifica el tráfico de registros de contenedores por método, contra el propio nombre de host del registro: GET y HEAD son un pull, PUT y POST son un push, DELETE es destructivo. GitHub, GitLab y Bitbucket están cubiertos tanto para la API REST como para git sobre HTTPS, donde git-receive-pack cuenta como escritura y queda bloqueado en modo Read-only mientras que los fetch siempre pasan. Un perfil que lee todo tu grafo de dependencias y no hace push de nada son dos ajustes.

El entorno es una lista que escribiste tú

Un perfil de Bromure ve las carpetas del Mac que compartiste con él, hasta ocho, cada una montada bajo /home/ubuntu. Todo lo demás de tu Mac no está montado, no es alcanzable y no es enumerable. El conjunto de credenciales es el que añadiste a ese perfil, así que un perfil que usas para construir un frontend no contiene credenciales de clúster que nadie pueda encontrar, ni ocultas ni de ninguna otra forma.

En el portátil de quien programaDIGITALOCEAN_ACCESS_TOKEN=dop_v1_…~/.config/doctl, ~/.docker/config.json,~/.git-credentials, ~/.aws, ~/.sshel agente se vuelve, una vezuna incidencia, una página, un README, un logahora lee lo que lees túun solo PUT a un buckety el artefacto que tus usuariosdescargan solos ya es suyoEn un perfil de BromureDIGITALOCEAN_ACCESS_TOKEN=brm_…cada dotfile está presente y cadasecreto que contienen es un señueloel valor real vive en el Maclo sustituye en el cable el proxy del host,para api.digitalocean.com y para nada másGuardrails: Read-only → 403la escritura se decide en macOS, yRequire approval antepone un diálogo
La misma credencial de escritura, en dos sitios. En un portátil es un token real en un archivo real, a un agente vuelto de distancia del artefacto que descargan tus usuarios. En un perfil de Bromure el token real no entra nunca en la VM, y la operación que haría la decide el host.

La descarga en ejecución también cruza el proxy

Toma ahora el lado de aguas abajo de la historia, donde tú eres uno de los 350.000. Lo que puedas atrapar ahí depende de dónde pongas la inspección.

El panel Supply Chain de Bromure filtra las descargas de paquetes en npm, PyPI, Cargo, RubyGems, Maven, NuGet, módulos de Go y Packagist: una barrera de edad activada por defecto que rechaza cualquier cosa publicada en los últimos dos días, consultas a OSV, socket.dev o Delpi como proveedor de filtrado, retirada de scripts de instalación que reescribe el tarball y arregla el hash de los metadatos del registro para que npm siga verificando, y un aviso antes de que pasen sin modificar los tarballs fijados por lockfile. Los .npmrc y pip.conf de dentro de la VM pueden apretar esas reglas y no pueden aflojarlas.

Esas reglas se ganan el sueldo por el sitio donde Bromure las aplica. El proxy del host las aplica a una petición y no a un paquete, así que un archivo JSON que un paso de compilación, una CLI, un servidor MCP o la propia aplicación traiga en ejecución desde el bucket de un proveedor cruza la misma frontera que un tarball. Todo lo que la VM le dice a la red la cruza.

Eso cambia el aspecto de las siete semanas. Con Session trace en Activity, Bromure registra el host, el estado, la latencia, el informe de sustitución y cualquier aviso de fuga para cada petición que hace la VM. Ponlo en Everything y guarda también los cuerpos, para cada host, cifrados con la misma clave del llavero que los secretos de tu perfil y legibles en el Trace Inspector. El día en que un feed del que depende tu compilación empieza a responder distinto es un diff que puedes abrir, en el Mac, en un registro sobre el que la VM no tiene acceso de escritura.

Inspección dentro del gestor de paquetesve: tarballs, versiones, lockfilesse ejecuta cuando instalas algoGET banner.json → el bucket del proveedorno es una instalación, así que no es un eventosiete semanas, ningún registroInspección en el proxy del hostve: cada petición que hace la VMedad, OSV, filtrado, retirada de scriptsGET banner.json → el bucket del proveedorhost, estado, latencia, informe de swapy el cuerpo, en el nivel Everything
Dónde ocurre la inspección decide lo que puede ver. Un control en el gestor de paquetes solo inspecciona cosas que se publicaron; un control en el proxy del host inspecciona cada petición que hace la VM, incluida la descarga en ejecución que llevó este payload.

Un feed envenenado es también, al final, texto que llega a una máquina. Cuando alcanza al agente como un tool_result, un archivo, una página descargada o un archivo de instrucciones como CLAUDE.md o AGENTS.md, el panel Prompt Injection lo puntúa en el propio dispositivo con un modelo local, y nada sale del Mac. Elige Ask me what to do y la petición se detiene mostrando el fragmento señalado; elige Block unilaterally y el agente recibe un 451 seco. Cuando quieras que la máquina desaparezca, Erase home reinicia /home/ubuntu y Reset to base vuelve a clonar el disco de sistema del workspace desde la imagen base.

BdThemes arreglará el escapado, el bucket recibirá claves más estrictas, y la próxima versión de esta historia llegará con otro proveedor y otro feed. Lo que dura es la disposición que hay debajo. Tu entorno de desarrollo guarda un puñado de credenciales, cada una de las cuales puede reescribir algo que miles de personas descargan de forma periódica, y ese mismo entorno ejecuta ahora un agente que lee texto de desconocidos todo el día.

La mayoría de los artículos de aquí pregunta qué debería tener permitido leer tu agente. Responde también a la otra pregunta, la que resulta incómoda en un portátil y fácil en un perfil: ¿qué tiene permitido escribir, y ante cuánta gente?

Instala Bromure Agentic Coding y dale al agente una máquina que no guarde la llave de nada de lo que publicas.