📋 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.
⚠️ 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/@v1repointed 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
↓
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
✅ Secure Code
✓ 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:
Static Review:
How to Test:
- Grep workflows for
pull_request_target+ PR-head checkout - Find
${{ }}event fields inlined intorun: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/@v1ran 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