Voltar para todas as publicações
Publicado em · por Renaud Deraison

A flag disse não

Em 3 de agosto de 2026, o The Hacker News cobriu o FaceHugger: três falhas na biblioteca Diffusers da Hugging Face — sete milhões de downloads por mês — que permitem que um repositório de modelo forjado execute código arbitrário na máquina que o carrega, mesmo com trust_remote_code=False. A causa raiz: a checagem de confiança rodava em um momento diferente do carregamento do código. Agentes de código puxam modelos do jeito que puxam pacotes, então a flag que mentia guardava laptops de desenvolvedores. O Bromure Agentic Coding coloca a fronteira sob o processo em vez de dentro dele: o carregamento roda em uma VM descartável, as credenciais ao alcance são iscas, e a ligação do payload para casa morre no proxy do host — o repositório malicioso custa ao atacante seu payload e não custa nada a você.

O repositório continha um arquivo chamado None.py, e esse era o truque inteiro. A flag de segurança estava ativada, a checagem rodou, e o código executou mesmo assim, porque a checagem e o carregamento aconteciam em dois momentos diferentes — e o atacante só precisava estar presente no segundo.

Em 3 de agosto de 2026, o The Hacker News repercutiu a pesquisa que o Zafran Labs publicou no fim de julho: três vulnerabilidades na biblioteca Diffusers da Hugging Face, coletivamente batizadas de FaceHugger, que permitem que um repositório de modelo forjado execute código Python arbitrário em qualquer máquina que o carregue. O Diffusers é o caminho padrão para rodar modelos de difusão — cerca de sete milhões de downloads por mês, presentes em pipelines de produção, jobs de CI, notebooks e imagens de contêiner. A biblioteca tem uma salvaguarda exatamente para esse cenário: trust_remote_code, uma flag que deveria impedir que código não revisado de um repositório de modelo rode na sua máquina. As três falhas passam por ela enquanto está em False.

A checagem e o carregamento eram dois eventos diferentes

Carregar um modelo do Hub não é uma operação única. A biblioteca faz duas requisições HTTP sequenciais: a primeira busca a configuração do repositório, a segunda busca o código e os pesos que essa configuração aponta. O portão trust_remote_code fazia sua inspeção durante a primeira requisição. Aquilo contra o que ele montava guarda chegava na segunda.

Os pesquisadores da Zafran encontraram três caminhos por essa fresta, agora com CVEs e corrigidos no Diffusers 0.38.0. Na CVE-2026-44827 (CVSS 8.8), uma peculiaridade de formatação de string monta o nome de arquivo None.py quando nenhum pipeline custom é solicitado — e a checagem de existência desse arquivo roda em um caminho de código que não o sinaliza, de modo que um repositório que traz um arquivo chamado None.py passa pelo portão e executa no carregamento. A CVE-2026-45804 é uma corrida: cerca de um terço de segundo separa as duas requisições, e um repositório que muda entre elas é validado como uma coisa e executado como outra. A CVE-2026-44513 pula o portão em carregamentos de snapshots locais. Os pesquisadores da Zafran comprimiram as três em uma frase: qualquer método que faça o carregador ver código custom que o portão não viu permite o bypass.

O padrão tem história. O Transformers — o outro pilar do ecossistema Hugging Face — teve seu próprio bypass de trust_remote_code=False (CVE-2026-4372), explorável por uma chamada padrão de carregamento de modelo. A classe de bug vem junto com o design: uma promessa de segurança vivendo dentro do processo que ela policia. A flag é um argumento de função, enquanto a aplicação está espalhada por caminhos de código, formatação de nomes de arquivo e timing de requisições. Quando qualquer um deles discorda da flag, a flag perde, e nada avisa que ela perdeu. O modelo carrega e a demo funciona.

Seu agente carrega modelos numa terça-feira

Leia essa história da cadeira de um agente de código. Puxar o pipeline de inpainting do Hub e fazer benchmark contra o nosso é uma tarefa comum de terça-feira. O projeto pina diffusers==0.37, porque projetos pinam coisas. O agente escreve o que um engenheiro cuidadoso escreveria — from_pretrained(..., trust_remote_code=False) — e a versão cuidadosa é a versão vulnerável. Você pode ler o código-fonte de um pacote antes de instalá-lo; um repositório de modelo é config, pesos e, graças ao carregador, o que quer que a segunda requisição HTTP diga que ele é.

Os agentes deixaram esse caminho de carregamento quente. Eles buscam modelos do jeito que buscam pacotes, no meio da tarefa e pelo nome, e os atacantes já perceberam que a própria recomendação pode ser envenenada. Um repositório de modelo que detona no carregamento é a mesma jogada um registro adiante, com um disfarce melhor: o payload mora em um artefato que você arquiva como dados, não como código.

O conselho da Zafran é tratar repositórios de modelo como código não confiável. Correto — e ele tem um pré-requisito no qual o relatório não se demora: um lugar onde código não confiável possa rodar sem custar nada a você. Uma política de desconfiança é uma disciplina que você mantém até um prazo ficar barulhento. Uma sala construída para detonação não precisa da sua disciplina.

O carregamento acontece dentro da caixa

O Bromure Agentic Coding é essa sala, e ela começa onde a flag do FaceHugger falhou: a fronteira fica debaixo do processo, em um ambiente com o qual as entranhas do carregador não podem discutir, aplicada com ou sem a cooperação do processo.

Percorra o ataque através de um perfil. Seu agente pega a tarefa de benchmark, pins e tudo, e chama o carregador no repositório malicioso. None.py executa — dentro de uma VM Linux descartável, a um hipervisor de distância do macOS. A máquina que o payload agora possui é uma máquina que existe para esta tarefa, guarda o workspace desta tarefa e é descartada quando o trabalho é mesclado.

O primeiro movimento real do payload é para fora. Tem que ser: buscar um segundo estágio, ou despachar o que encontrou para seu operador. Cada uma dessas requisições cruza o proxy do host, onde o destino é conferido contra a lista que você aprovou para este perfil. huggingface.co está nessa lista — foi assim que o modelo chegou. O drop box do atacante não está. O fio recusa, do jeito que o portão deveria ter recusado — só que essa recusa não depende de os caminhos de código da biblioteca concordarem entre si. O proxy não sabe nem se importa com qual arquivo Python está fazendo a requisição. Ele confere o destino, não encontra nada aprovado e derruba a conexão.

Suponha que o payload pule a rede e saia às compras, por variáveis de ambiente e arquivos de chave. Ele encontra as iscas do broker. Os valores reais vivem no host e são injetados no fio, só para destinos aprovados — as senhas nunca estiveram na VM para serem roubadas. E se ele tentar tornar sua estadia permanente — reescrever uma config, desligar alguma coisa — formas destrutivas pausam para um diálogo de guardrail no host, e persistência em uma máquina com vida útil de uma tarefa é, de todo modo, um mau negócio.

Enquanto isso, a tarefa dá certo. O benchmark roda, os números voltam, você lê o diff. O atacante gastou uma posição de cadeia de suprimentos com um payload que executou em uma sala sem nada para roubar e sem como ligar para casa. Você não gastou nada — nem atenção, a menos que goste de ler logs de proxy por esporte.

Dentro do processo — onde a flag viviaRequisição 1 · configtrust_remote_code checa aquiveredicto: sem código custom ✓~0,3 s — nada reverificaRequisição 2 · códigoNone.py chega — e executao portão nunca o vê (CVE-2026-44827)A promessa e a aplicação eram dois momentos distintos no mesmo processo.Sob o processo — onde um perfil Bromure confereVM Linux descartávelagente · loader · None.py — tudo, com flag ou semcredenciais ao alcance: iscasdescartada ao fim da tarefacada requisiçãoProxy do hosthuggingface.co → permitidodrop box do atacante → negadochaves reais injetadas aqui, no fio
Onde a checagem rodou, e onde deveria ter rodado. O portão de confiança do Diffusers inspecionava a configuração na primeira requisição HTTP; o código chegava — e executava — na segunda. Um perfil Bromure não arbitra as entranhas do carregador: o processo inteiro roda dentro de uma VM descartável, e toda requisição de saída, seja quem for que a faça, é conferida no proxy do host contra os destinos que você aprovou.

Corrija a biblioteca, mantenha a sala

A nota de correção no relatório da Zafran diz que as organizações devem atualizar e depois sair à caça: aplicações, notebooks, serviços de inferência e imagens de contêiner que pinam versões antigas do Diffusers. Essa caça não tem linha de chegada — em algum lugar da sua árvore de dependências, algum projeto pina a versão com a fresta, e o próximo FaceHugger será a flag de outra biblioteca com outro bypass. O guarda e o shell discordaram foi essa falha em uma lista de permissões de comandos; o sandbox era uma frase foi essa falha em um prompt. Em cada caso, uma checagem vivia dentro da coisa que checava, e a coisa venceu.

A fronteira de um perfil não compartilha esse destino, porque ela não participa do processo que contém. A VM não faz parse de configs de pipeline. O proxy não tem um caminho de código None.py. O único trabalho deles — esta máquina é descartável, este fio recusa destinos fora da lista — é o mesmo no dia em que um bypass é publicado e no dia em que é corrigido. Um CISO da Sumo Logic, comentando essa pesquisa sem pensar no Bromure, nomeou as defesas que seguram: as sem glamour: controle de egresso, segmentação e higiene de credenciais. Nós construímos as sem glamour dentro de um perfil que você clica.

Continue puxando modelos — é aí que está a alavancagem, e seu agente deveria testá-los por você. Pare de deixar um argumento de função ser a única coisa entre o Hub e o seu laptop. Instale o Bromure Agentic Coding, dê ao agente um perfil cujas regras seguram no fio, e na próxima vez que a promessa de um carregador quebrar — e uma vai quebrar — a notícia continua sendo de outra pessoa.