Volver a todas las publicaciones
Publicado el · por Renaud Deraison

La flag dijo que no

El 3 de agosto de 2026, The Hacker News cubrió FaceHugger: tres fallos en la biblioteca Diffusers de Hugging Face — siete millones de descargas al mes — que permiten a un repositorio de modelo manipulado ejecutar código arbitrario en la máquina que lo carga, incluso con trust_remote_code=False activado. La causa raíz: la comprobación de confianza corría en un momento distinto al de la carga del código. Los agentes de código traen modelos igual que traen paquetes, así que la flag que mentía custodiaba laptops de desarrolladores. Bromure Agentic Coding pone la frontera bajo el proceso en lugar de dentro de él: la carga corre en una VM desechable, las credenciales al alcance son señuelos, y la llamada a casa del payload muere en el proxy del host — el repositorio malicioso le cuesta al atacante su payload y a ti no te cuesta nada.

El repositorio contenía un archivo llamado None.py, y ese era todo el truco. La flag de seguridad estaba activada, la comprobación corrió, y el código se ejecutó de todos modos, porque la comprobación y la carga ocurrían en dos momentos distintos — y el atacante solo tenía que estar presente en el segundo.

El 3 de agosto de 2026, The Hacker News recogió la investigación que Zafran Labs publicó a finales de julio: tres vulnerabilidades en la biblioteca Diffusers de Hugging Face, bautizadas colectivamente FaceHugger, que permiten a un repositorio de modelo manipulado ejecutar código Python arbitrario en cualquier máquina que lo cargue. Diffusers es la vía estándar para ejecutar modelos de difusión — unos siete millones de descargas al mes, asentados en pipelines de producción, jobs de CI, notebooks e imágenes de contenedor. La biblioteca tiene una salvaguarda para exactamente este escenario: trust_remote_code, una flag que se supone impide que código sin revisar de un repo de modelo corra en tu máquina. Los tres fallos pasan por delante de ella mientras está en False.

La comprobación y la carga eran dos eventos distintos

Cargar un modelo desde el Hub no es una sola operación. La biblioteca hace dos peticiones HTTP secuenciales: la primera trae la configuración del repositorio, la segunda trae el código y los pesos que esa configuración señala. La puerta trust_remote_code hacía su inspección durante la primera petición. Aquello contra lo que montaba guardia llegaba en la segunda.

Los investigadores de Zafran encontraron tres caminos por esa brecha, ya con CVE asignados y parcheados en Diffusers 0.38.0. En CVE-2026-44827 (CVSS 8.8), una rareza del formateo de cadenas construye el nombre de archivo None.py cuando no se solicita ningún pipeline custom — y la comprobación de existencia de ese archivo corre por una ruta de código que no lo señala, de modo que un repositorio que incluye un archivo llamado None.py cruza la puerta y se ejecuta al cargar. CVE-2026-45804 es una carrera: cerca de un tercio de segundo separa las dos peticiones, y un repositorio que cambia entre ambas se valida como una cosa y se ejecuta como otra. CVE-2026-44513 se salta la puerta en cargas de snapshots locales. Los investigadores de Zafran comprimieron los tres en una frase: cualquier método que haga que el cargador vea código custom que la puerta no vio permite el bypass.

El patrón tiene historia. Transformers — el otro pilar del ecosistema de Hugging Face — tuvo su propio bypass de trust_remote_code=False (CVE-2026-4372), explotable mediante una llamada estándar de carga de modelo. La clase de bug viene con el diseño: una promesa de seguridad que vive dentro del proceso que vigila. La flag es un argumento de función, mientras que la aplicación está repartida entre rutas de código, formateo de nombres de archivo y timing de peticiones. Cuando cualquiera de ellos discrepa de la flag, la flag pierde, y nada te dice que perdió. El modelo carga y la demo funciona.

Tu agente carga modelos un martes cualquiera

Lee esa historia desde la silla de un agente de código. Trae el pipeline de inpainting del Hub y haz un benchmark contra el nuestro es una tarea normal de martes. El proyecto fija diffusers==0.37, porque los proyectos fijan cosas. El agente escribe lo que escribiría un ingeniero cuidadoso — from_pretrained(..., trust_remote_code=False) — y la versión cuidadosa es la versión vulnerable. Puedes leer el código fuente de un paquete antes de instalarlo; un repo de modelo es config, pesos y, gracias al cargador, lo que sea que diga la segunda petición HTTP.

Los agentes han puesto caliente esa ruta de carga. Traen modelos igual que traen paquetes, en mitad de la tarea y por nombre, y los atacantes ya descubrieron que la propia recomendación puede ser envenenada. Un repo de modelo que detona al cargar es la misma jugada un registro más allá, con mejor disfraz: el payload se aloja en un artefacto que archivas como datos, no como código.

El consejo de Zafran es tratar los repositorios de modelos como código no confiable. Correcto — y tiene un prerrequisito en el que el informe no se detiene: un lugar donde el código no confiable pueda correr sin costarte nada. Una política de sospecha es una disciplina que mantienes hasta que una fecha límite se pone ruidosa. Una sala construida para la detonación no necesita tu disciplina.

La carga ocurre dentro de la caja

Bromure Agentic Coding es esa sala, y empieza donde falló la flag de FaceHugger: la frontera se asienta debajo del proceso, en un entorno con el que las entrañas del cargador no pueden discutir, aplicada coopere o no el proceso.

Recorre el ataque a través de un perfil. Tu agente toma la tarea del benchmark, pins incluidos, y llama al cargador sobre el repositorio malicioso. None.py se ejecuta — dentro de una VM Linux desechable, a un hipervisor de macOS. La máquina que el payload ahora posee es una máquina que existe para esta tarea, contiene el workspace de esta tarea y se descarta cuando el trabajo se mergea.

El primer movimiento real del payload es hacia fuera. Tiene que serlo: traer una segunda etapa, o enviar lo que encontró a su operador. Cada una de esas peticiones cruza el proxy del host, donde el destino se comprueba contra la lista que aprobaste para este perfil. huggingface.co está en esa lista — así llegó el modelo. El buzón del atacante no lo está. El cable se niega, como debería haberse negado la puerta — salvo que esta negativa no depende de que las rutas de código de la biblioteca estén de acuerdo entre sí. El proxy no sabe ni le importa qué archivo Python hace la petición. Comprueba el destino, no encuentra nada aprobado y corta la conexión.

Supón que el payload se salta la red y se va de compras, por variables de entorno y archivos de claves. Encuentra los señuelos del broker. Los valores reales viven en el host y se inyectan en el cable, solo para destinos aprobados — las contraseñas nunca estuvieron en la VM para ser robadas. Y si intenta hacer permanente su estancia — reescribir una config, apagar algo — las formas destructivas se pausan ante un diálogo de guardrail en el host, y la persistencia en una máquina con una vida útil de una tarea es, de todos modos, un mal negocio.

Mientras tanto, la tarea sale bien. El benchmark corre, los números vuelven, tú lees el diff. El atacante gastó una posición de cadena de suministro en un payload que se ejecutó en una sala sin nada que robar y sin forma de llamar a casa. Tú no gastaste nada — ni siquiera atención, a menos que disfrutes leyendo logs de proxy por deporte.

Dentro del proceso — donde vivía la flagPetición 1 · configtrust_remote_code revisa aquíveredicto: sin código custom ✓~0,3 s — nada reverificaPetición 2 · códigoNone.py llega — y se ejecutala puerta nunca lo ve (CVE-2026-44827)La promesa y la aplicación eran dos momentos distintos del mismo proceso.Bajo el proceso — donde revisa un perfil BromureVM Linux desechableagente · loader · None.py — todo, con flag o sin ellacredenciales al alcance: señuelosdesechada al acabar la tareacada peticiónProxy del hosthuggingface.co → permitidobuzón del atacante → denegadoclaves reales inyectadas aquí
Dónde corrió la comprobación, y dónde debería haber corrido. La puerta de confianza de Diffusers inspeccionaba la configuración en la primera petición HTTP; el código llegaba — y se ejecutaba — en la segunda. Un perfil Bromure no arbitra las entrañas del cargador: el proceso entero corre dentro de una VM desechable, y cada petición saliente, la haga quien la haga, se comprueba en el proxy del host contra los destinos que aprobaste.

Parchea la biblioteca, quédate con la sala

La nota de corrección del informe de Zafran dice a las organizaciones que actualicen y luego salgan de caza: aplicaciones, notebooks, servicios de inferencia e imágenes de contenedor que fijan versiones antiguas de Diffusers. Esa caza no tiene línea de meta — en algún lugar de tu árbol de dependencias, algún proyecto fija la versión con la brecha, y el próximo FaceHugger será la flag de otra biblioteca con otro bypass. El guardia y el shell discreparon fue este fallo en una lista de comandos permitidos; el sandbox era una frase fue este fallo en un prompt. En cada caso, una comprobación vivía dentro de la cosa que comprobaba, y la cosa ganó.

La frontera de un perfil no comparte ese destino, porque no participa en el proceso que contiene. La VM no parsea configs de pipeline. El proxy no tiene una ruta de código None.py. Su único trabajo — esta máquina es desechable, este cable rechaza destinos fuera de la lista — es el mismo el día en que se publica un bypass que el día en que se parchea. Un CISO de Sumo Logic, comentando esta investigación sin pensar en Bromure, nombró las defensas que aguantan: las poco glamurosas: control de egreso, segmentación e higiene de credenciales. Nosotros construimos las poco glamurosas dentro de un perfil que tú clicas.

Sigue trayendo modelos — ahí está la palanca, y tu agente debería probarlos por ti. Deja de permitir que un argumento de función sea lo único entre el Hub y tu laptop. Instala Bromure Agentic Coding, dale al agente un perfil cuyas reglas aguanten en el cable, y la próxima vez que la promesa de un cargador se rompa — y alguna se romperá — la noticia seguirá siendo de otro.