A potential supply chain attack on GitHub CodeQL started simply: a publicly exposed secret, valid for 1.022 seconds at a time.
In that second, an attacker could take a series of steps that would allow them to execute code within a GitHub Actions workflow in most repositories using CodeQL, GitHub’s code analysis engine trusted by hundreds of thousands of repositories. The impact would reach both public GitHub (GitHub Cloud) and GitHub Enterprise.
If backdooring GitHub Actions sounds familiar, that’s because it’s exactly what threat actors did in the recent tj-actions/changed-files supply chain attack. Imagine that very same supply chain attack, but instead of backdooring actions in tj-actions, they backdoored actions in GitHub CodeQL.
An attacker could use this to:
- Compromise intellectual property by exfiltrating the source code of private repositories using CodeQL.
- Steal credentials within GitHub Actions secrets of workflow jobs using CodeQL and leverage those secrets to execute further supply chain attacks.
- Execute code on internal infrastructure running CodeQL workflows.
- Compromise GitHub Actions secrets of workflows using the GitHub Actions Cache within a repo that uses CodeQL.
This is the story of how we uncovered an exposed secret leading to a race condition, a potential supply chain attack, and CVE-2025-24362.
Note: Per GitHub’s advisory, they have found no evidence of compromise to its platform or systems.
How Did We Get Here?
In January 2025, I took a break from Praetorian’s Red Team and began three months of research. I aimed to push the limits of public GitHub Actions exploitation, building on presentations we’ve given at Black Hat, DEF CON, Schmoocon, and Black Hat Arsenal. Tools and takeaways from this research will be implemented in our CI/CD Professional Services Engagements, and into Chariot, our Continuous Threat Exposure Management platform.
I began my research rotation by scanning GitHub Actions workflow artifacts for secrets.
Secret Scanning
In August 2024, Palo Alto researcher Yaron Avital published an article about identifying secrets in workflow artifacts. I had a hunch that there were still secrets to be found, especially since there hadn’t been much public follow-up work since the article.
I built a simple Actions Artifacts Secret Scanner to get started. It downloads artifacts from GitHub Actions workflows, recursively extracts their contents, and scans their contents for secrets with Nosey Parker, Praetorian’s open-sourced secrets scanning tool.
The Actions Artifacts Secret Scanner has been integrated into Chariot and open-sourced as a Gato capability.
After running this scanner for one day, it found a secret that could lead to a supply chain attack on GitHub CodeQL.
But first, I needed to see if the key was usable.
Background
CI/CD vulnerabilities sound complicated until you understand the terminology. Let’s catch you up.
GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows the execution of code specified within workflows as part of the CI/CD process. When you push code to a GitHub repository or create a pull request, GitHub Actions can automatically build, test, and deploy your code using workflows defined in YAML files.
For example, let’s say you are building a web application that is hosted in AWS. You can configure a GitHub Actions workflow so that whenever you push code to your repository, it is automatically tested and then deployed to AWS.
If you are new to GitHub Actions, we’d recommend reading through some examples.
Every workflow run generates a GITHUB_TOKEN — a special, automatically generated GitHub App installation token that allows the workflow to interact with the repository. This token’s permissions can be configured in the workflow file, at the repository level, or at the org level, determining what actions it can perform within the repository.
Put simply:
- GitHub workflows execute on GitHub runners (typically a VM or Docker containers).
- GitHub runners need a way to authenticate to GitHub to do stuff the workflows tell them to do.
- For that purpose, they use the GITHUB_TOKEN.
If the token has high privileges, then token compromise == bad.
What are Workflow Artifacts?
We found the publicly exposed secret in a GitHub Actions workflow artifact.
GitHub Actions workflows can upload workflow “artifacts” to GitHub Actions. Workflow artifacts can be any file and are saved by that workflow for later use. By default, artifacts are publicly accessible to anyone with read access to the repository and are stored for up to 90 days.
And Finally, What is CodeQL?
CodeQL is GitHub’s Code Analysis Engine. The CodeQL actions perform static code analysis on GitHub repositories to try and identify vulnerabilities. They have found several hundred CVEs over it’s lifetime, protecting organizations from breaches.
Security tools, like CodeQL, often need access to sensitive systems and data, making them an attractive target to an attacker.
If CodeQL was compromised, one of the most widely used security tools now becomes a backdoor.
Finding the Token
After running the Actions Artifact Secrets Scanner for a day, it picked up a token in a github/codeql-action repository artifact published by this run. The Actions Artifact Secrets Scanner downloaded the “my-debug-artifacts” zip uploaded by the “PR Check – Debug artifacts after failure” workflow, recursively extracted the “my-db-java-partial.zip” file stored inside, and ran Nosey Parker. Within seconds, Nosey Parker flagged a GitHub Token starting with “ghs_” in a crash report.
Investigating manually, I confirmed this was a GitHub App token installation token stored in a file containing the environment variables of the GitHub Runner executing the workflow.
Investigating Impact
Secrets compromise is cool, but what can we do with this token? The impact of a compromised
GITHUB_TOKEN is minimal if it only has read permissions.
The easiest way to determine the privileges of a GITHUB_TOKEN is to look at workflow logs. To investigate this, I navigated to the “Setup Job” step of the workflow that uploaded the token.
The GitHub token had full write privileges.
We could spend a lot of time talking about each privilege, but let’s focus on the ones that are particularly interesting.
Contents: write – Allows the token to create branches, create tags, and upload release artifacts.
Actions: write – Allows you to work with Actions, including trigger workflow_dispatch events.
Packages: write – Allows the token to upload packages.
With these privileges, an attacker has a lot of potential for repository tampering, but there is still one issue. These tokens are only valid for the duration of their specific workflow job. That means that once the job is over, the token is useless. Three things needed to happen for an attacker to be able to abuse this token:
- The token needs to have some sort of write privileges (already confirmed).
- The token needs to use V4 of the upload artifact API, as that is the only version that allows you to retrieve an artifact before the job is complete (and after the job is complete, the token is invalid.)
- The time between uploading the artifact and completing the job needs to be great enough for us to download, extract, and use the token.
If all of these conditions are met, this publicly exposed token could be used to launch a full scale supply chain attack on CodeQL. This was like finding out that the security guard was accidentally leaving their master key in plain sight for a brief moment, over and over again.
We had to determine if the guard left us enough time to steal the key and use it before they returned to their post.
Let’s investigate further. Tick, tock.
Determining the Artifact Upload Version
Identifying the artifact upload version is typically straightforward. If a workflow uses
actions/upload-artifact@v4***, we can retrieve the artifact before job completion. If it uses an earlier version, we cannot do so.
In this case, CodeQL wasn’t using the actions/upload-artifact action; they were manually using the upload artifact client in the source code. Code comments indicated it used version 4. That was enough for me to continue.
Now we needed to determine if the job lasted long enough for us to retrieve and use the token.
Calculating our Execution Time
Looking at the raw GitHub logs for this workflow, we can see two key timestamps:
“Finalizing artifact upload” occurred at 17:22:09:888.
The final step in the job, “Cleaning up orphan processes”, happened at 17:22:10:911.
That means we had approximately 1.022 seconds to download the artifact, extract the GitHub token, and use it. I noticed the token stayed valid for about a second after the “Cleaning up orphan processes” step, so we’ll call it two seconds.
The guard was giving us two seconds to steal the key and use it before they returned.
Is that enough time for an attacker to use this token? Or is this another theoretical vulnerability?
Start Your Engines
To test this, I made a Python script artifact_racer.py. Artifact racer performs the following actions.
- Continuously queries the github/codeql-action GitHub repository until it sees a “PR Check – Debug artifacts after failure” workflow begin.
- Monitors the running workflow for artifacts.
- Once it sees a “PR Check – Debug artifacts after failure” workflow run, it downloads the artifact and extracts the GITHUB_TOKEN.
- Shelling out for file operations and downloads was key to increasing the speed, although there are probably ways to make it even faster.
- Uses the GITHUB_TOKEN to make a new branch.
- Use the GITHUB_TOKEN to push an empty file named
poc.txt
to that branch. - Makes a new tag for that commit.
If I could make a new branch, add a file, and create a tag for that commit, that would prove an attacker could use the token for nefarious purposes before it expired.
Given that the workflow artifact was only ~21MBs, I thought we had a chance. After successfully executing against a test repository, I moved on to the github/codeql-action repository.
Executing the Proof of Concept
I ran the racer.
And then I waited.
About two hours later, a “PR Check – Debug artifacts after failure” workflow executed. The racer successfully retrieved the GITHUB_TOKEN, created the branch, pushed the file, and added the tag.
Branch URL: https://github.com/github/codeql-action/tree/testpoc
Commit URL: https://github.com/github/codeql-action/commit/26fcd8e2368067be04a705a229590749a426fefe
Tag URL: https://github.com/github/codeql-action/releases/tag/testpoctag
The ability to create a tag becomes very important in this attack. Keep that in mind as we go.
After confirming the GITHUB_TOKEN could be used within the short time window, we responsibly disclosed this vulnerability to GitHub.
What if You’re Still Not Impressed?
Using the GITHUB_TOKEN, an attacker could add malicious code to any unprotected branch. A covert tactic would be to target feature branches pre-merge, smuggle in a small malicious code change, and wait for it to get merged. This would be especially effective due to how frequently the GitHub Actions bot commits to the CodeQL Actions repository.
They could also add tags that point to specific commits. For example, if they had malicious code on a branch and then added a v3 tag, anyone who manually used codeql-action…@v3 would execute the malicious code. More on this later.
Through code execution, you’d be able to compromise any GitHub Actions secret used within that job, as well as exfiltrate the source code of that repository. If their actions were executing on internal infrastructure, which is common with self-hosted GitHub runners, you’d also have code execution on their internal network or cloud environment.
The impact from this attack would have been very similar to the recent tj-actions/changed-files supply chain attack.
This impact is impressive, but it doesn’t quite live up to the claims I made in the beginning. Yes, through these paths, they could launch a supply chain attack against repos manually using the CodeQL actions. However, most organizations don’t include these actions manually. They just go into their repository settings, click “Enable CodeQL”, and go from there.
At first, I assumed that enabling CodeQL in your repository didn’t interact with the github/codeql-action repository at all.
I was wrong.
Exponential Impact
After discussing this issue with some colleagues, I decided to investigate further. What actually happens when you enable CodeQL?
This section is key to understanding the full impact of this vulnerability. Stick with me.
To investigate, I created my own public repository, “John’s Top Secret Repo”, and enabled CodeQL.
After you enable CodeQL with the default settings, a special GitHub Actions workflow runs in your repository. This CodeQL action won’t show up in your repository workflows, but you can navigate to the workflow logs to see what it is doing.
Enabling CodeQL in your repository settings.
Observing the CodeQL workflow.
Based on my observations, CodeQL:
- Checks out your repository to the filesystem
- Initializes CodeQL
- Runs CodeQL scans
- Uploads the scan results
Let’s take a closer look at step 3.
If this doesn’t shock you, look again. Remember that we have the ability to push tags to the github/codeql-action repository.
CodeQL, under the hood, is executing the actions in the github/codeql-action repository, using the commit referenced by the v3 tag. This tag was not immutable, and they were not using workflow pinning (which GitHub recommends), which meant that an attacker could overwrite the v3 tag using the compromised GITHUB_TOKEN. Now, if an attacker removed and then added a v3 tag to their malicious commit, every single repository using the default CodeQL workflow would execute their malicious code.
The Action created when selecting “Advanced CodeQL” also used the reusable github/codeql-action with the v3 tag.
The CodeQL actions check out the source code of every repository they run on, which means that a malicious CodeQL action could exfiltrate the source code of any repository using default CodeQL configurations.
This would result in significant disclosure of intellectual property. And if you’ve ever operated on a Red Team, you know how many hardcoded secrets are lying around in private source code repositories.
But Wait, There’s More
We’re almost done. But remember, I promised one more thing:
4. Compromise GitHub Actions secrets of any workflow using the GitHub Actions Cache within a repo that uses CodeQL
When assessing the impact of CI/CD attack paths, I look for ways to compromise GitHub Actions secrets. Usually, those secrets are where the crown jewels live.
If the CodeQL action is executing with write privileges or alongside GitHub Actions secrets, then it’s trivial to use the code execution to exfiltrate those secrets. But the default CodeQL action uses a GITHUB_TOKEN that only has read privileges, so you can’t perform repository write operations, backdoor releases, or use fancy workflow dispatch events to steal secrets, like what happened with PyTorch.
What the default CodeQL action does do is execute in the main branch of the repository. The main branch of any GitHub repository can write cache entries that will be used by the entire repo. This opens up an opportunity to conduct GitHub Actions cache poisoning.
GitHub Actions Cache Poisoning is thoroughly explained in this article by Adnan Khan, which documents the discovery and exploitation of cache poisoning. The easiest way to conduct GitHub Actions cache poisoning is by deploying Cacheract, malware that persists in a build pipeline through cache poisoning.
If an attacker deployed Cacheract in the CodeQL workflow, it would:
- Predict cache entries
- Overwrite these entries with a malicious action
- Gain code execution within any workflow that uses
action-cache
(the Actions Cache is used by most repositories) - Leverage code execution to compromise GitHub Actions secrets used by those workflows, capture privileged GITHUB_TOKENs, and more
Even if someone noticed the malicious CodeQL action and remediated the vulnerability, Cacheract would continue poisoning caches.
I spent ten minutes looking for prominent repos that use CodeQL & actions/cache and identified
Homebrew, Angular, and Grafana.
Cache poisoning would allow an attacker to leverage this CodeQL supply chain attack to gain write access to repositories and repository secrets.
Congratulations, You Made It
We’ve now hit all the impact highlights I mentioned at the beginning:
- Compromise intellectual property by exfiltrating the source code of all private repositories using CodeQL.
- Steal credentials within GitHub Actions secrets of any workflow job using CodeQL, and leverage those secrets to execute further supply chain attacks.
- Execute code on internal infrastructure running CodeQL workflows.
- Compromise GitHub Actions secrets of any workflow using the GitHub Actions Cache within a repo that uses CodeQL.
Supply chain attacks like these are scary, especially when they start with something as simple as a publicly exposed credential. If this is your first time hearing about abusing GitHub Actions to launch supply chain attacks, I’ll let you in on a secret: these vulnerabilities occur all the time.
GitHub Actions abuse has been around for several years, but it is still one of the highest-impact, least-understood vulnerability classes. That is slowly starting to change (emphasis on ~slowly~). The DevOps and security communities need to commit to learning about these vulnerabilities to protect their organizations from risk. Vulnerabilities like this, and the recent tj-actions/changed-files supply chain attack, are starting to bring these issues to the public spotlight. That is why we invest in research to uncover these vulnerabilities and design solutions to prevent them.
CVE-2025-24362
A side-effect of this disclosure was CVE-2025-24362. The publicly exposed GITHUB_TOKEN was within a debug artifact uploaded by the CodeQL Action after a failed code scanning workflow. The CodeQL Actions repository was intentionally triggering this failure, but other users of CodeQL Actions could have exposed their own secrets as environment variables to the workflow, had their workflows experienced a similar failure.
This issue was fixed in CodeQL Action version 3.28.3.
Even though this disclosure resulted in a CVE, the highest potential impact still lies in exploiting that vulnerability against the CodeQL Actions repository and launching a supply chain attack against CodeQL users.
Remediation
GitHub had one of the most rapid and impressive remediation responses we have ever seen.
Jan 22, 2025, 3:13 PM UTC: Report Submitted to GitHub
Jan 22, 2025, 5:48 PM UTC: GitHub acknowledged receipt of the submission
Jan 22, 2025, 6:28 PM UTC: GitHub confirmed the vulnerability, temporarily disabled the “PR Check – Debug artifacts after failure” workflow, and submitted this PR to disable debug artifacts upload. This occurred just three hours after submitting the report, which is a very rapid resolution time.
Jan 24, 2025: GitHub assigned CVE-2025-24362 and published this security advisory, which notes they found no evidence of compromise to its platform or systems.
If you are concerned about you’re own GitHub Actions workflow artifacts, you can take the following steps to limit the risk of secrets exposure:
- ideally, only upload specific files or directories as workflow artifacts
- avoid uploading artifacts containing environment variables, the
.git/config
file, or any files in the runner’s<path_to_runner_dir>/_work/_temp/
directory - limit GITHUB_TOKEN permissions to read-only
- scan artifacts for secrets prior to uploading
To learn more about this vulnerability, how I discovered it, and how you can detect similar vulnerabilities in your own environment, please join me for a webinar on April 10 at 1pm ET.
How Can Praetorian Help
Praetorian has been leading the charge in offensive CI/CD security for several years, inventing novel tooling and giving presentations at Black Hat, DEF CON, Schmoocon, and Black Hat Arsenal.
Our Continuous Threat Exposure Management (CTEM) platform, Chariot, can identify vulnerabilities like this in your attack surface before the attackers do.
Our CI/CD Security Assessments can take an in-depth look at your internal CI/CD security posture, enumerating attack paths that an attacker would exploit to go from low-privileged access to complete organization compromise.
The next major supply chain attack could start with something as simple as a publicly exposed secret. Help make sure that doesn’t happen by learning about CI/CD vulnerabilities and implementing continuous controls to protect your organization from compromise.
You can create a free Chariot account anytime. Alternatively, if you’re interested in our managed Chariot offering, reach out to speak with our team.