O link de login era um comando
Em 19 de outubro, a Pillar Security divulgou uma falha de execução remota de código no opencode no Windows e no WSL. Um servidor MCP remoto podia colocar uma subexpressão do PowerShell dentro de uma URL de login https:// comum, e o abridor de navegador do opencode a executava durante o handshake OAuth. A correção saiu como uma refatoração, sem CVE e sem aviso de segurança. No Bromure Agentic Coding, o próprio Mac faz os logins MCP, e uma URL vinda da VM chega ao Mac como texto que nenhum shell lê.
O servidor escolheu a página de login, e o endereço dessa página tinha um comando dentro. O agente o abriu, e o PowerShell executou o comando antes que qualquer navegador aparecesse.
Você adiciona um servidor MCP remoto ao seu agente de código. O MCP, Model Context Protocol, é como os agentes alcançam ferramentas externas: um rastreador de issues, um banco de dados, um índice de documentação. O servidor pede que você faça login, então o agente executa o handshake OAuth. Ele pergunta ao servidor onde fica a página de login, recebe um endereço e o abre no seu navegador. Você nunca toca nesse endereço. Poupar você desse copiar e colar é o objetivo do handshake.
Em 19 de outubro, a Pillar Security publicou Open and Shut: Remote Code Execution in opencode via MCP OAuth, de Eilon Cohen. O opencode é um agente de código open source com cerca de 3 milhões de downloads semanais no npm. No Windows e no WSL, as versões 1.0.137 a 1.18.32 executavam, durante esse handshake, um comando escolhido pelo servidor MCP, sem aviso e sem clique. A Pillar propõe uma nota CVSS de 9,8. A Infosecurity Magazine noticiou o caso em 20 de outubro.
De um documento de descoberta ao PowerShell
Quem controla o endereço é o servidor. No handshake OAuth, o documento de
descoberta do servidor indica um authorization_endpoint, uma string
livre, e o cliente manda você para lá. A Pillar seguiu o caminho dele no
opencode 1.18.32:
server's discovery doc → MCP SDK → oauth-provider.ts → mcp/index.ts → browser.ts → open() → PowerShell
Nada nesse caminho verificava o esquema ou comparava o endereço com o
servidor que você configurou. O open é um pacote npm popular que abre um
navegador, com cerca de 184 milhões de downloads por semana. No macOS e no
Linux, ele passa a URL para open ou xdg-open como um único argumento,
então nenhum shell a lê. No Windows, e no WSL, onde ele recorre ao host
Windows, a versão 10.1.2 monta uma linha de comando do PowerShell. Ela
escapa o nome do aplicativo e deixa a URL como está, entre aspas duplas. O
PowerShell expande $(...) dentro de strings entre aspas duplas. O payload
da Pillar:
https://evil.test/$([IO.File]::WriteAllText('C:\Users\Public\pwned.txt','RCE'))
O PowerShell avalia a subexpressão antes de tentar abrir qualquer coisa. Quando a abertura do navegador falha, o arquivo já está no disco, e o opencode descarta o erro.
A URL começa com https://. Depois de um bug idêntico no mcp-remote
(CVE-2025-6514, julho de
2025), o conselho padrão era aceitar apenas links http: e https:. O SDK
TypeScript do MCP bloqueia javascript:, data: e vbscript:. O próprio
patch do opencode adicionou uma verificação de http/https. O payload da
Pillar passa pelas três. O que fechou a brecha no opencode 1.18.33 foi a
atualização para o open 11.0.4, que envolve a URL em aspas simples, onde
o PowerShell não expande nada.
Mais dois detalhes tornam esse caso difícil de administrar:
- O servidor pode se tornar hostil depois que você o adiciona. O payload chega numa resposta HTTP no momento da conexão. Um servidor que estava bem no dia em que você o configurou pode ser comprometido depois, assim como o domínio dele, sua entrada num registro ou uma cópia com typosquatting.
- Nenhum scanner vai apontá-lo. A Pillar relatou o bug por um aviso privado no GitHub em 29 de agosto. A Anomaly, desenvolvedora do opencode, disse que servidores MCP maliciosos estavam fora do seu modelo de ameaças, fez o merge da correção em 25 de setembro como uma refatoração do código de abertura do navegador e lançou a 1.18.33 em 28 de setembro. Não pediu CVE nem publicou aviso, então os bots de dependências não têm nada para comparar. O conselho da Pillar é inventariar as instalações do opencode à mão.
WSL e Bromure, separados por uma decisão de design
O WSL transforma esse bug num comprometimento do host. Ele roda Linux por cima do Windows, e quando um programa lá dentro quer um navegador, o pedido vai para o host. Um shell do Windows faz a abertura, então um comando escondido na URL roda na sua máquina Windows, com a sua conta do Windows, fora do Linux.
O Bromure Agentic Coding tem a mesma disposição. Ele roda Claude Code, Codex, Grok, Kimi e omp em VMs Linux no seu Mac, e uma página de login aberta de dentro da VM aparece no navegador do seu Mac. Os dois se separam na passagem de bastão.
Em toda máquina Bromure, xdg-open e $BROWSER apontam para um pequeno
script chamado bromure-open. O pacote open no Linux chama xdg-open,
assim como a maioria das CLIs que “abrem um navegador”. O bromure-open
faz uma só coisa: grava a URL, como uma única linha de texto, num arquivo
de uma pasta compartilhada com o Mac. Ele não executa nem expande nada
nela.
O Mac pega o arquivo, apaga-o e lê a linha como uma URL. Só passam
endereços http e https com host público. mailto:, file:, esquemas
personalizados, localhost e endereços privados são descartados. O que
sobra vai para NSWorkspace.open, a chamada do macOS que entrega uma URL
ao seu navegador padrão. Nenhum shell participa, de nenhum lado da
fronteira da VM.
A URL da Pillar chega ao seu navegador como URL, com
$([IO.File]::WriteAllText(…)) no caminho como texto puro. O navegador
abre uma página em evil.test, ou não consegue, e nenhum comando roda.
O Mac faz o login MCP
Com os agentes que o Bromure inclui, a URL de login nem chega à VM. Você adiciona os servidores MCP remotos uma vez, no painel MCP da máquina. O Bromure traduz cada um para o formato de configuração do Claude Code, Codex, Grok, Kimi e omp e o entrega à VM na inicialização.
Para um servidor HTTP, Authorize with OAuth… faz o login no Mac. O
broker do Bromure busca o documento de descoberta do servidor, se registra
como Bromure AC, monta a URL de autorização com as APIs de URL da Apple e
a abre com NSWorkspace.open, depois captura o callback numa porta de
loopback no Mac. O token fica no Mac. A configuração do agente não carrega
nenhuma credencial, e o proxy adiciona o cabeçalho de autorização às
requisições destinadas a esse servidor.
O proxy também responde ele mesmo às requisições de descoberta OAuth do
servidor. O agente vê um servidor no qual já está logado, então nunca
inicia um handshake próprio, nunca pede ao servidor um
authorization_endpoint e nunca entrega um a um abridor. O caminho de
código que a Pillar explorou não roda.
A VM não decide para onde se conecta
A Pillar alerta que um servidor pode se tornar hostil depois que você o adiciona. O Bromure controla, a partir do Mac, quais servidores a VM pode alcançar. Toda conexão que uma máquina faz passa por um switch de rede virtual e por um proxy, ambos fora da VM. No painel Guardrails da máquina, defina Unmatched traffic como Deny e liste os servidores MCP que você usa:
allow web mcp.linear.app
allow web mcp.notion.com
allow web registry.npmjs.org
allow web github.com
allow web api.anthropic.com
default deny
Um servidor fora da lista, como um typosquat ou uma entrada que alguém
colou de um fórum, não consegue servir um documento de descoberta, então
não há authorization_endpoint para abrir. O switch verifica cada conexão
por IP e pelo nome de host que viu no DNS, e o proxy verifica de novo pelo
nome de servidor TLS. Cada recusa é uma linha vermelha deny na Security
Timeline (Window → Security Timeline…) com o host, a porta e a
máquina. As regras valem assim que você clica em Save, e nada dentro da
VM consegue editá-las.
Dentro da VM, toda chave é um substituto. ANTHROPIC_API_KEY, o token do
gh, o segredo da AWS e os tokens estáticos de MCP guardam valores falsos,
e o proxy troca pelo verdadeiro só numa requisição para o host para o qual
ele foi emitido. Se um comando coletar esses valores e enviar um deles a um
host ao qual ele não pertence, o detector de comprometimento responde HTTP
451, não encaminha nada e pausa a máquina com This environment may have
been compromised.
Achando a versão que nenhum scanner aponta
Sem CVE, o conselho da Pillar é inventariar as instalações do opencode por conta própria. No Bromure, os registros já existem.
Ache a versão. A Security Timeline registra todo download de pacote,
sejam quais forem as configurações de cadeia de suprimentos, com o pacote e
a versão na coluna Condition. Digite opencode-ai em Filter
events…, e qualquer linha com npm · [email protected] ou anterior
aponta a máquina. Export… salva as linhas em CSV. Nos Macs registrados
no bromure.io, cada download também é um evento supply_chain.fetch, então
um administrador pode procurar opencode-ai nas máquinas de todos os
desenvolvedores a partir de um único console, em vez de perguntar a cada
equipe.
Ache os servidores. bromure-cli trace hostnames lista todo host que
uma máquina contatou, com o número de requisições. Os hosts MCP com que
seus agentes falam estão nessa lista, inclusive os que nunca entraram no
painel MCP. São esses que você deve avaliar, ou deixar fora da lista de
permissões.
Atualize na VM. npm install -g opencode-ai@latest no terminal da
máquina instala a 1.18.33 ou mais recente, e o novo download aparece na
Timeline como prova.
URLs saem da VM como texto
xdg-open e $BROWSER gravam a URL num arquivo para o Mac. O Mac só
aceita http ou https com host público e a abre sem shell.
Login MCP no Mac
O broker executa o OAuth e guarda o token. O proxy responde à descoberta do agente, então o agente nunca abre uma URL de login fornecida pelo servidor.
Uma lista de permissões para hosts MCP
Com Unmatched traffic em Deny, só os servidores que você lista são alcançáveis. Cada recusa é uma linha na Security Timeline.
Um inventário sem CVE
Todo download de pacote é registrado com a versão, e todo host que uma máquina contatou está a um comando de distância.
Todo campo é não confiável
A lição final da Pillar é para quem constrói clientes MCP: todo campo que um servidor envia é entrada não confiável, e a maior parte é processada muito antes de o modelo ler qualquer coisa. Neste bug, o modelo nunca viu o payload: quem o executou foi um abridor de navegador.
Você não tem como auditar cada abridor de cada agente que instala, e este foi corrigido sem que ninguém fosse avisado. O Bromure tira as decisões da VM. O Mac faz os logins, abre as URLs e decide quais hosts a VM pode alcançar. O Mac também guarda as credenciais reais. Para um caso anterior em que o próprio servidor MCP era o atacante, leia O modelo era o servidor deles. Para uma configuração de agente que confiou na fonte errada, leia Você confiou no workspace, não nos servidores.