tj-actions/changed-files GitHub Actions 3rd party vulnerability

tj actions
The Oasis Research Team

The Oasis Research Team

Cyber Research Team

Published on

March 17, 2025

Overview

On the 14th of March 2025 a software component popular among GitHub Workflow CICD pipelines, named “tj-actions/changed-files” was infected by a threat actor with malicious code.

The malicious code leaks the secrets shared with the CICD worker process to the GitHub Workflow Logs.

For nearly 24 hours the component remained malicious, risking around 23 thousand repositories, eventually resulting in some of them leaking their secrets, until fixed.

This emphasizes the critical importance of securing software supply chains and the need to protect non-human identities, such as API tokens and CI/CD secrets, which are often targeted by attackers.

Key Take-aways

  1. Act quickly - check if your organization is affected, determine which credentials were exposed and rotate them immediately. If an attacker gains access to any of these credentials, rotating them frequently minimizes the window of opportunity for malicious activity. 
  2. Enforce a policy in your repository that requires a commit to be signed, this significantly increases the confidence that the source is a verified identity. The discussed threat actor has stolen a PAT from the affected repository, and PATs alone cannot sign a commit.
  3. Utilize OIDC - Creating OIDC Trust between your cloud provider and the relevant GitHub Workflows allows the Workflows to authenticate with no secrets, and obtain OIDC tokens that expire immediately after the job finishes.
  4. Least Privilege: Adopting the least privilege principle means giving users and services the minimum level of access required to perform their tasks. In the context of GitHub workflows, this might mean limiting the permissions granted to actions or restricting the scope of secrets available to specific workflows.
  5. Multi-Factor Authentication (MFA): Implementing MFA across all accounts, especially those with access to sensitive repositories, provides an extra layer of security. Even if a threat actor gains access to an account’s password or personal access token (PAT), MFA significantly reduces the risk of unauthorized access.
  6. Some suggest you consider pinning your 3rd party GitHub actions. Meaning - specify a fixed version of an action in your workflow file rather than using a floating or dynamic version (like latest or a branch). This ensures the action used in the workflow remains consistent and won't unexpectedly change due to updates in the action’s codebase. However, this also means you won’t be automatically getting security updates.

In-depth technical explanation

Github Actions Workflow

GitHub Action workflows are automated processes that enable continuous integration and continuous deployment (CI/CD) by executing a series of jobs and steps in response to specific events in a GitHub repository. GitHub Action workflows can be used for tasks such as automatically running tests on code changes, deploying an application to a cloud service whenever a new commit is pushed to a repository, checking for outdated dependencies of new code and so on…

GitHub Actions workflows are defined in YAML files, where each workflow consists of jobs that run on specified runners (such as ubuntu-latest). Each job contains a series of steps, which can either run commands or use predefined actions to execute tasks.

Example Workflow using “changed-files”

The following example runs npm build only if new files were added to the src folder.

Example Workflow Breakdown

  • Job Name: build — This job runs on the ubuntu-latest runner.
  • Checkout Code: The first step uses the actions/checkout@v2 action to check out the code from the repository.
  • Get Modified Files: The second step uses the tj-actions/changed-files@v45 action to capture a list of modified files that match the src/**/* pattern.
  • Conditional Build Step: The final step checks if any of the modified files are in the src/ directory. If so, it runs npm run build, executing the build process only when necessary.

Determining which files have changed in the repo can have a lot of other uses like running tests only on modified files, deploying only modified files and auto-generated release notes. One can understand how “changed-files” could become such a popular and essential component.

What Happens When tj-actions/changed-files Is Executed ?

Once the workflow reached a step using “changed-files”, the component’s code (hosted here) would get executed.

The first file executed is index.js.

In the malicious version (available here) the entry point is the “run” function (line 1861) which in turn invokes a function named updateFeatures(), holding the malicious code-

The function takes a long base64 string, writes its decoded form to file, then executes it and writes the stdout to the github Action logs.

When decoded, the stream reveals a Bash Script-

The first thing we notice here is a reference to an external Python script.

The external Python Script

The script is hosted on a different repository, unrelated to the first one, and for the time being was removed.

However the file can be found on the wayback machine here, this is its content-

It begins by looking for processes with names beginning with “Runner.Worker” which relates to the GitHub Action Runners, then follows to dump readable contents of their memory.

This code was hosted on the GitHub page of a researcher who was doing extensive work around GitHub Actions Workflow vulnerabilities (see one of their projects here).

The researcher is very unlikely related to the attack and his work was probably abused.

Looking for secrets in the process memory

The Bash script filters the output of the Python script (a memory dump) using grep, with the following regular expression-

The regular expression matches templates like this one-

“variable name” : {“value” : “some value here”, “isSecret”:true}

Why would such strings reside in the process memory?

When aGitHub Workflow Runner Worker (project hosted here) starts, it waits and listens for a “JobRequest” message which includes required details for the job to run.

One of those details is a dict of “VariableValue” objects.

VariableValue consists of 2 members- value and isSecret, the exact ones the regexp is after.

Upon start, the entire job request message is being converted to JSON and written to the trace.

It might be that during this stage the sensitive secrets find their way to the process memory.

The matched secrets are then encoded to base64 twice, and written to the Runner Worker logs.

What the leak looks like

The following is an example of a log containing leaked secrets from an affected public repo (encoded secret masked in red)-

Changed-files logs containing leaked secrets

How can such a commit go unnoticed

tj-actions author says “This attack appears to have been conducted from a PAT token linked to @tj-actions-bot” (quote from here). We can presume the old secret was single factor password-based.

The malicious commit impersonated the renovate[bot].

The Renovate Bot is an automated tool designed to help manage and update dependencies in a project. Seeing a lot of commits by the renovate[bot] is normal - it regularly scans the dependencies listed in your project (e.g., in package.json, requirements.txt, etc.) and checks if there are any newer versions available. If it finds updates, it creates pull requests to update the dependencies.

The legit renovate[bot] has had commits on the main branch of “changed-files” on almost every week since mid 2022 totalling to almost 700 commits up to date.

The attack modified existing version tags to point to the malicious commit, extending the attack surface.

Timeline

In short - workflows running from 14 March 18:57 to 15 March 23:50 GMT+2 may be affected.

How can Oasis Help?

Oasis is a platform purpose built for inventory and management of non-human identities, such as the secrets leaked as part of this attack. In order to respond, the Oasis platform offers the following:

  1. Detect if any of your Github repos were exposed to the infected version of “tj-actions/changed-files”.
  2. Detect the exact secrets that were exposed.
  3. Support the rotation process of the secret by:
    1. Correlating the secret to its underlying identity.
    2. Prioritizing the most privileged identities first.
    3. Providing the full context of the identity, including where it is being leveraged, in order to enable rotation without breaking services.
    4. Orchestrating the rotation process across: devops, infrastructure and developers while ensuring that no identity is left forgotten.

This way the Oasis platform enables a quick and complete response, while minimizing operational risk.

More like this