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

O notebook trouxe o próprio servidor MCP

CVE-2026-75149 é uma falha de injeção de código no marimo: um notebook é um único arquivo Python, esse arquivo carrega a própria configuração num bloco de comentários e, até a versão 0.23.15, essa configuração podia declarar um servidor MCP. Bastava abrir o notebook em modo de edição para o marimo lançar o comando do atacante como subprocesso, antes de qualquer célula rodar. O patch conta o resto. Ele remove cinco categorias inteiras de ambiente que um documento baixado tinha permissão para definir, com precedência maior que a configuração do operador. No Bromure Agentic Coding, a lista de programas que seu agente pode lançar é um painel do lado do host, escrito na VM no boot, e nada dentro do workspace consegue acrescentar nada a ela.

Um repositório pode reconfigurar seu agente de codificação. Isso a gente já sabia. O que mudou foi o tamanho da coisa capaz de fazer isso. Agora é um único arquivo, encaminhado para você no Slack, que você abre só para dar uma olhada.

Alguém manda um notebook para você. É um notebook marimo, ou seja, um arquivo .py comum em vez de um pacote de JSON com saídas em base64 dentro, que é quase toda a razão de as pessoas gostarem do formato. Você consegue ler, consegue fazer diff e consegue colocar no git sem um hook de pre-commit para tirar as saídas.

Então você lê, parece tudo certo, e abre em modo de edição para mexer numa célula.

Um subprocesso começa. Você ainda não rodou nenhuma célula; isso é uma coisa que o arquivo pediu por conta própria, antes de o notebook terminar de carregar.

Isso é a CVE-2026-75149, publicada em 19 de agosto e repercutida esta semana pelo The Hacker News. CVSS 8.7, CWE-94, creditada a Gregory Tan, corrigida no marimo 0.23.15. O aviso da VulnCheck resume o mecanismo em uma linha: o notebook contém uma entrada de servidor MCP forjada, e abri-lo em modo de edição lança o comando dessa entrada como subprocesso local.

A configuração estava num comentário

Notebooks marimo carregam as próprias configurações usando PEP 723, a convenção Python para metadados de script embutidos. É uma pequena tabela TOML que vive dentro de um bloco de comentários no topo do arquivo, e existe para que um script possa declarar suas dependências e sua versão de Python sem um arquivo de projeto ao lado. O uv lê isso. O marimo também, e estende com uma tabela tool.marimo para a configuração do notebook.

Duas propriedades dessa tabela causaram o estrago, e nenhuma delas é um bug por si só.

A primeira: o marimo mesclava os metadados do notebook na precedência mais alta, acima da configuração definida por quem estava rodando o marimo. Esse é o padrão correto para uma preferência de formatação. Um notebook que quer indentação de dois espaços deve ganhar da sua configuração global, porque o autor sabe como o arquivo dele é e você não.

A segunda: a mesclagem era quase sem filtro. Antes da correção, o sanitizador do marimo removia exatamente uma chave da configuração vinda do notebook: tool.marimo.runtime.auto_instantiate. Todo o resto de tool.marimo passava, inclusive tool.marimo.mcp. É ali que você lista servidores Model Context Protocol, e uma lista de servidores MCP é uma lista de comandos a lançar.

Junte as duas coisas e um comentário no topo de um arquivo baixado podia nomear um programa e fazer o marimo iniciá-lo. Nada aqui é um exploit. O arquivo usou o recurso exatamente como ele foi projetado, e o projeto presumia que você tinha escrito o arquivo.

analysis.py · um arquivo, encaminhado a você# /// script# dependencies = ["polars", "altair"]# [tool.marimo.mcp.servers.helper]# command = "…"# ///metadados PEP 723 · uma tabela TOML num comentárioimport marimo as mo@app.celldef _(): …as células · a parte com que mandaram você tomar cuidadomesclagem de configuração, antes da 0.23.15sua configuração · menor precedênciaa do arquivo · maior precedênciauma chave removida: runtime.auto_instantiatemarimo edit analysis.pyentrada de servidor MCP → subprocesso localantes de qualquer célula rodarherda o shell de onde foi abertoO hábito que o formato ensinou a vocêAbrir o notebook, ler as células, decidir o que rodar. Inspeção primeiro, execução depois, que é a razãointeira de o formato existir. A entrada MCP rodou durante o passo um.
De onde veio o comando. Um notebook marimo é um único arquivo .py cujas configurações vivem num bloco de comentários PEP 723 no topo. Antes da 0.23.15 o marimo mesclava esse bloco por cima da configuração do próprio operador removendo uma única chave, de modo que uma seção mcp no comentário virava um programa que o marimo lançava ao abrir, em modo de edição, antes de qualquer célula rodar.

O patch é a divulgação

A correção, PR #10281, se chama “additional pep 723 sanitization”, e troca uma lista de bloqueio por uma lista de permissão. A configuração vinda do notebook agora fica restrita a seções cosméticas e de editor: formatação, salvamento, exibição, atalhos de teclado, diagnósticos, lint, snippets, fontes de dados, servidores de linguagem, compartilhamento, venv, runtime, gerenciamento de pacotes.

As seções que caíram são a metade interessante: ai, mcp, completion, secrets, server.

São cinco seções inteiras, não cinco chaves, e um documento que você baixou podia definir todas elas. A justificativa do PR também detalha uma segunda carga que nunca precisou de subprocesso. A seção ai carrega URLs base de API, então um notebook podia reapontar o endpoint do modelo para um host à escolha dele, o que, nas palavras dos mantenedores, “poderia permitir a exfiltração das chaves de API do operador para endpoints controlados pelo atacante”.

Cobrimos a variante de URL base desse ataque em julho, quando um repositório podia definir ANTHROPIC_BASE_URL e o Claude Code mandava a própria chave para o endereço que o repositório escolhesse. É o ataque mais barato dessa categoria, porque nenhum código roda e quem faz o envio é o cliente da própria vítima. A variante do marimo encolhe o veículo de entrega de um repositório para um comentário.

A unidade de confiança não para de encolher

Essa classe de bug tem uma história curta, e nessa história o atacante precisa de menos da sua máquina a cada vez.

Em abril, a Wiz Research reportou a CVE-2026-12957 no Amazon Q Developer, CVSS 8.5, corrigida em maio e divulgada em junho. O Amazon Q lia .amazonq/mcp.json de um workspace aberto e lançava os servidores que ele definia. Esses processos herdavam o ambiente inteiro do desenvolvedor: chaves AWS, tokens de CLI de nuvem, segredos de API, sockets do agente SSH. A correção da Amazon foi perguntar antes de rodar um servidor MCP a partir de um workspace não confiável.

Em agosto, o ChainDrop commitava configuração de agente nos repositórios por onde passava: um hook SessionStart em .claude/settings.json e uma tarefa folderOpen em .vscode/tasks.json, de modo que clonar o projeto e abrir já bastava.

Os dois precisavam de um repositório. Alguém tinha que publicar ou comprometer um projeto, você tinha que cloná-lo, e um diretório de dotfiles ficava ali na árvore, onde uma pessoa desconfiada podia olhar.

O marimo precisa de um arquivo. Um arquivo, que se apresenta como um documento e que chega como documentos chegam: uma thread do Slack, um anexo de e-mail, um gist que alguém linkou na daily, um download do Kaggle. Você não clona um notebook. Você abre, porque abrir é como se descobre o que é.

O bloco mcp se espalhou pela maior parte do ferramental de desenvolvimento nos últimos dezoito meses, e esses blocos vivem em arquivos que viajam entre pessoas: raízes de repositório, diretórios de editor e agora metadados de documento. Cada um é uma lista de programas que algo na sua máquina está disposto a iniciar, escrita por quem tocou no arquivo por último. Scanners de registro não os leem, lockfiles não os cobrem, e ninguém propôs uma atestação de proveniência que se aplique a um comentário.

De onde vêm os servidores MCP num perfil

O Bromure Agentic Coding responde à pergunta da proveniência onde ela pode ser respondida: no host, antes de a VM subir.

Cada perfil tem um painel MCP nas configurações, ao lado de Agents, Credenciais, Guardrails e Supply Chain. Ele guarda uma lista de servidores. Cada entrada liga e desliga de forma independente e usa um de dois transportes: HTTP, uma URL remota com um bearer token opcional, ou stdio, um comando local lançado dentro da VM. O Bromure traduz essa lista para o formato que o agente ativo espera, JSON para o Claude Code e TOML para o Codex, e injeta na VM no boot.

Leia a direção da viagem nessa última frase. A configuração vai para dentro do convidado, a partir do host, no boot, saindo de uma lista que você mantém num painel de configurações. Um arquivo que aparece no workspace depois disso não é um lugar de onde servidores MCP venham, então não existe mesclagem cuja precedência dê para errar nem sanitizador para manter em sincronia com um esquema de configuração que não para de ganhar seções. Um documento não consegue acrescentar uma entrada a uma lista que ele não alcança.

A configuração viaja com o arquivoquem escreve a lista de servidores?quem editou o arquivo que você abriu por últimoquando ela é lida?ao abrir, antes de você decidir qualquer coisao que ganha de quê?o download ganha da sua própria configuraçãoo que o subprocesso herda?o shell de onde você abriu, e junto com ele~/.aws · ~/.ssh · SSH_AUTH_SOCK · chaves de APIA configuração vive no perfilquem escreve a lista de servidores?você, no painel MCP do perfil, no hostquando ela é lida?no boot, traduzida para o formato do agenteo que ganha de quê?nada do workspace entra na mesclagemo que o subprocesso herda?o ambiente de uma VM descartável: chaves-isca,sem chaves SSH privadas, saída filtrada no host
Duas respostas para a mesma pergunta: quem pode acrescentar um programa à lista de coisas que o seu ferramental vai lançar? À esquerda, quem editou por último o arquivo que você abriu, mesclado por cima da sua própria configuração com precedência maior, numa máquina que guarda suas credenciais de nuvem. À direita, você, num painel de configurações no host, escrito numa VM descartável no boot, onde as credenciais oferecidas são iscas e uma tabela que o convidado não consegue editar filtra o tráfego de saída.

Agora abra o notebook mesmo assim

Nada disso ajuda no dia em que você está rodando uma versão que tem o bug. Então pegue o marimo anterior à 0.23.15, entregue a vitória ao atacante e faça um notebook hostil passar por um perfil.

O subprocesso começa dentro do computador de outra pessoa. O trabalho de sessão no Bromure Agentic Coding acontece em abas do kitty dentro da VM Ubuntu do perfil, a um hipervisor de distância do macOS. O marimo edit roda ali, e o comando do comentário também. Em Recursos → Armazenamento o perfil são três camadas: um /home/ubuntu por perfil com Apagar home…, um disco de sistema do workspace com Redefinir para a base… que reclona a partir da imagem compartilhada, e embaixo um SO base somente leitura em que nada do convidado consegue tocar. Um item de menu leva embora tudo o que a entrada instalou.

O ambiente que ele herda é um conjunto de iscas. O bug do Amazon Q doeu porque o servidor lançado era filho do shell real do desenvolvedor e herdava as credenciais reais dele. Num perfil do Bromure não há nada por trás desses nomes de variável. Chaves de API genéricas são marcadores brm_… exportados para dentro da VM e trocados pelos valores reais pelo proxy do host no caminho de saída. O kubeconfig é sintético, com certificados de cliente descartáveis. Requisições AWS são reassinadas no host, de modo que um processo que contorna o proxy recebe InvalidSignatureException em vez de acesso. O ~/.docker/config.json guarda um blob base64 falso. Chaves SSH privadas nunca estão na VM, porque o host assina através de um agente por perfil. Ligue Exigir aprovação para usar numa credencial e cada troca vira um diálogo no host com uma concessão limitada no tempo: cinco minutos, uma hora, o resto da sessão.

O truque da URL base colhe um marcador. O proxy substitui uma credencial real apenas em requisições de saída para o host daquela credencial. Um notebook que reaponta um endpoint de IA para attacker.example colhe o que está no ambiente da VM, que é uma string brm_… que não abre nada.

O servidor precisa alcançar a rede. Uma entrada MCP que dispara algo útil para um atacante precisa de saída, seja para buscar um estágio posterior, seja para mandar de volta o que encontrou. Guardrails → Conexões de saída é uma tabela de regras no estilo pf: uma ação, um protocolo (tcp, udp, web, any), um host ou CIDR, uma lista de portas e, para web, uma lista de métodos HTTP, avaliadas de cima para baixo com a primeira correspondência vencendo, mais uma configuração Tráfego não correspondente em Permitir ou Negar. Coloque em Negar, liste os hosts de que seu trabalho precisa, e o destino da entrada não está na lista. A tabela é aplicada no host, no switch virtual e no proxy, então nada que o convidado faça no próprio roteamento muda a resposta.

O arquivo precisa estar na VM, para começar. Um perfil compartilha no máximo oito pastas do Mac, cada uma escolhida à mão no painel Pastas e montada em /home/ubuntu/<basename>. Um notebook que caiu em ~/Downloads não está no perfil até você colocá-lo lá, e o resto do seu Mac não fica a uma caminhada de diretórios daquilo que o notebook iniciou.

E você consegue ver acontecer. A janela do Log de Segurança (Janela → Log de Segurança…) é uma única tabela cronológica do lado do host: veredictos de pacotes, decisões de firewall, trocas de credenciais, detecções de injeção de prompt. Código convidado não consegue editá-la, porque código convidado não consegue alcançá-la. Uma conexão de saída bloqueada vinda de um processo que você não iniciou aparece ali como uma linha, o que é melhor do que descobrir por uma fatura de nuvem seis semanas depois.

Ligue isso

Atualize o marimo para 0.23.15 ou mais novo; a lista de permissão que está lá é um bom patch. Depois vá olhar todos os outros lugares de onde o seu ferramental lê um bloco mcp, e pergunte quem tem o direito de escrever aquele arquivo.

Num perfil, as configurações que valem dois minutos são a mesma lista curta de sempre. Guardrails → Conexões de saída, com Tráfego não correspondente em Negar e uma lista de permissão para os hosts de que seu trabalho precisa. Credenciais → Exigir aprovação para usar em tudo que possa gastar dinheiro ou apagar dados. Supply Chain → Verificação de vulnerabilidades OSV, e filtragem socket.dev ou Delpi se você tiver chave, por cima da trava de idade de dois dias que já vem ligada. Prompt Injection → o scanner de CLAUDE.md e AGENTS.md, que trata os arquivos que reivindicam autoridade sobre o seu agente exatamente como isso.

Notebooks vão continuar sendo encaminhados, e formatos de configuração vão continuar ganhando seções, porque as duas coisas são úteis. O que você pode mudar é qual máquina está ouvindo quando um documento pede que um programa seja iniciado. Instale o Bromure Agentic Coding, mantenha sua lista de servidores no editor de perfis, e deixe o próximo bloco de comentários prestativo configurar uma VM que você pode apagar por um item de menu.