CA da eMudhra comprometida: o que fazer no seu app Bun
Todo fetch("https://...") que o seu app Bun faz confia em algumas centenas de empresas que você nunca escolheu. Uma delas acabou de ter problema. Foi publicado um relato de que uma CA intermediária da eMudhra, autoridade certificadora indiana que está nos principais repositórios de confiança, foi comprometida. O site que reúne o caso tem um nome que resume bem o clima: emudhra.we-have-a-problem.com. A investigação ainda está em andamento, então os detalhes do incidente podem mudar. A parte que interessa para quem escreve backend em Bun já dá para explicar: o que uma CA comprometida faz com o seu código, e por que o runtime não te protege tanto quanto o navegador.
O que é uma CA intermediária e por que ela é tão sensível
Revisão rápida. Quando o seu código abre uma conexão TLS, o servidor manda um certificado. Esse certificado foi assinado por uma CA intermediária. A intermediária, por sua vez, foi assinada por uma CA raiz. A raiz fica guardada numa lista dentro do seu sistema, do navegador ou do runtime.
O cliente sobe essa cadeia. Se chegar numa raiz conhecida, a conexão é aceita.
O detalhe que assusta: qualquer intermediária confiável pode emitir certificado para qualquer domínio. Não importa se o api.stripe.com usa outra CA. Se alguém controla a chave privada de uma intermediária da eMudhra, essa pessoa consegue gerar um certificado válido para api.stripe.com, para o seu banco de dados gerenciado ou para o endpoint de login do seu app. O cliente vê uma cadeia correta e segue a vida.
A segurança do TLS é tão forte quanto a CA mais fraca da sua lista.
Para isso virar ataque, o invasor ainda precisa ficar no meio do caminho: Wi-Fi público, DNS sequestrado, rede corporativa, provedor comprometido. Não é trivial. Também não é ficção científica.
Navegador e Bun reagem de jeitos diferentes
Aqui está o ponto que pouca gente lembra.
Quando uma intermediária é comprometida, os navegadores têm reação rápida. O Firefox usa CRLite, o Chrome usa CRLSets. São listas de revogação que chegam por atualização silenciosa, às vezes em horas. O usuário nem percebe.
Runtimes de servidor funcionam diferente. Node e Bun trazem a lista de raízes embutida no binário, derivada do repositório da Mozilla (NSS). E, por padrão, nenhum dos dois consulta revogação: não tem OCSP, não tem CRL, não tem CRLite. A verificação acontece assim:
- A cadeia chega numa raiz que está no binário? Passou.
- O certificado está dentro da validade? Passou.
- O hostname bate? Passou.
Ninguém pergunta se aquela intermediária foi revogada ontem.
Isso quer dizer que, enquanto a raiz da eMudhra continuar dentro do binário do seu Bun, um certificado emitido pela intermediária comprometida pode ser aceito pelo seu backend. E a correção só chega até você quando duas coisas acontecem: a Mozilla tirar ou restringir a raiz, e você atualizar o Bun para uma versão que já traga a lista nova.
Minha opinião, sem rodeio: a maioria dos times trata a versão do runtime como detalhe de infraestrutura que "atualiza quando der". Nesse tipo de incidente, essa versão define em quem o seu servidor confia. A imagem Docker com oven/bun:1.1 esquecida em produção há um ano carrega uma lista de confiança de um ano atrás.
Como saber se o seu runtime confia na eMudhra
O Bun implementa as APIs node:tls e node:crypto, então dá para olhar a lista de raízes embutida em poucas linhas:
// check-roots.ts
import { rootCertificates } from "node:tls";
import { X509Certificate } from "node:crypto";
const suspeitas = rootCertificates
.map((pem) => new X509Certificate(pem))
.filter((cert) => /emudhra/i.test(cert.subject));
for (const cert of suspeitas) {
console.log(cert.subject.replace(/\n/g, ", "));
console.log(" válido até:", cert.validTo);
}
console.log(`Bun ${Bun.version}: ${suspeitas.length} raiz(es) eMudhra`);
bun check-roots.ts
Rode isso na sua máquina e, mais importante, dentro da imagem que vai para produção. Se aparecer alguma raiz da eMudhra, o seu runtime confia em toda a cadeia dela. Isso não quer dizer que você está sendo atacado. Quer dizer que você está exposto até a correção chegar.
Uma ressalva: se você liga o uso do repositório de certificados do sistema operacional (algumas versões recentes do Bun e do Node têm opção para isso), o resultado também depende do pacote ca-certificates da sua distro. Aí são duas listas para manter atualizadas, não uma.
O que fazer no seu projeto agora
Não precisa entrar em pânico nem reescrever nada. Precisa de um pouco de higiene.
1. Atualize o Bun e fique de olho nos próximos releases. Quando a Mozilla agir, a mudança entra no bundle de certificados do runtime. Acompanhe o changelog e atualize a imagem base. Se você fixa versão no Dockerfile (e deveria), coloque um lembrete para revisar.
# antes: esquecido desde o ano passado
FROM oven/bun:1.1
# depois: versão explícita, revisada com frequência
FROM oven/bun:1.3
2. Para integrações críticas, restrinja a confiança. Se o seu backend conversa com poucos serviços sensíveis (gateway de pagamento, API interna, banco), você não precisa confiar no mundo inteiro para essas chamadas. O fetch do Bun aceita opções de TLS:
const caInterna = await Bun.file("./certs/ca-interna.pem").text();
const res = await fetch("https://api.interna.minhaempresa.com/pedidos", {
tls: {
ca: caInterna, // só aceita certificados dessa CA
},
});
Para serviço interno, isso resolve o problema por completo. Uma intermediária comprometida da eMudhra não assina nada que essa CA aceite.
Para serviços de terceiros, dá para ir além com checkServerIdentity e conferir a impressão digital da chave pública (pinning). Funciona, mas cobra um preço: quando o provedor trocar a chave, a sua integração quebra às 3 da manhã. Só use se o provedor publicar as chaves e você tiver alerta para isso.
3. Não confunda isso com rejectUnauthorized: false. Já vi muito código que, diante de qualquer erro de TLS, desliga a verificação "só para testar". Num momento como esse, esse código é o pior lugar onde você pode estar. Faça uma busca no repositório:
grep -rn "rejectUnauthorized: false\|NODE_TLS_REJECT_UNAUTHORIZED" src/
Se aparecer algo fora de teste, corrija hoje. É de graça.
4. Cuidado com NODE_EXTRA_CA_CERTS. O Bun respeita essa variável. Ela é útil para proxy corporativo, mas cada CA extra que você adiciona é mais uma empresa (ou mais um servidor de TI) que pode emitir certificado para qualquer domínio. Confira o que está configurado nos seus ambientes.
E se eu não uso Bun no backend?
O raciocínio vale igual para Node, Deno e qualquer outro runtime que traga a própria lista de raízes. Em React Native, o app usa o repositório do sistema do celular. Isso depende de atualização do Android ou do iOS, e aparelho Android antigo sem atualização carrega a lista velha para sempre. Nesse caso, pinning de certificado para a sua própria API passa a fazer mais sentido.
O navegador é, ironicamente, o cliente mais protegido dessa história. Quem tem mais a perder é o backend que ninguém atualiza.
Por que isso continua acontecendo
O sistema de CAs funciona porque a maioria das autoridades faz o trabalho direito, e porque existe o Certificate Transparency: todo certificado público precisa ser registrado em logs abertos. É isso que permite que alguém descubra emissão indevida e monte um site como esse da eMudhra. Sem CT, um certificado falso podia ficar anos em uso sem ninguém perceber.
Você também pode usar isso a seu favor. Serviços como o crt.sh deixam você buscar todos os certificados emitidos para o seu domínio. Se aparecer um emissor que você não reconhece, é sinal de alerta. Configurar um monitor desses leva cinco minutos e já pegou muita gente no flagra.
Minha opinião
Esse incidente não é motivo para trocar de runtime nem para sair fazendo pinning em tudo. É um lembrete de algo que a gente esquece: confiança em TLS é dependência, igual a qualquer pacote do package.json. Ela tem versão, envelhece e precisa de atualização. Rode o script de verificação, atualize a imagem base e restrinja a CA nas integrações internas. Uma tarde de trabalho, e você para de depender da sorte.
Se quiser ver como eu organizo isso nos meus projetos com Bun, dá uma olhada em alguns projetos que mantenho.
Resumo para o LinkedIn
Seu backend Bun pode estar aceitando certificado falso agora, e o navegador estaria te protegendo melhor. Uma CA intermediária da eMudhra foi comprometida. Qualquer intermediária confiável pode emitir certificado para qualquer domínio, inclusive o da sua API de pagamento. O navegador recebe revogação em horas. Node e Bun não consultam revogação nenhuma: confiam na lista de raízes que veio embutida no binário. Aquela imagem oven/bun:1.1 esquecida em produção confia no mundo de um ano atrás. O que eu fiz: rodei um script de 10 linhas para ver se o runtime confia na eMudhra, atualizei a imagem base e restringi a CA nas integrações internas. Leva uma tarde. Confiança em TLS é dependência, igual a pacote do package.json. Tem versão e envelhece. Escrevi o passo a passo com o código no blog. Quem quiser conferir o próprio ambiente, o link está nos comentários. #Bun #NodeJS #Seguranca #Backend #TLS