We spend a lot of time building walls around what exists: branch protection rules, mandatory code reviews, signed commits and releases. Layer upon layer around things we keep alive. But we rarely ask the deeper question: should this exist at all?
Every branch in a repository is a door. Every door can be knocked on. And some of those who knock are in disguise: pull requests that look helpful, that pass a superficial review, but that bring along modified CI configurations, altered build scripts, or smuggled-in workflow commands. The pipeline trusts the code, but the code does not always deserve that trust.
This is Poisoned Pipeline Execution. It is not a theoretical scenario, and it requires no privileged access. It needs only a target: a branch that accepts pull requests, connected to a pipeline that runs whatever it is given.
What is Poisoned Pipeline Execution?
Poisoned Pipeline Execution (PPE) is classified as CICD-SEC-4 in the OWASP Top 10 CI/CD Security Risks. These attacks abuse the trust relationships firmly embedded in the pipelines of modern software development projects. Unlike classic application vulnerabilities, PPE exploits the automation and the trust in development workflows to execute malicious code with the full privileges of the CI/CD system.
Security researchers distinguish three variants:
Direct PPE (D-PPE): The attacker directly modifies CI configuration files such as .github/workflows/, .gitlab-ci.yml or Jenkinsfile. This happens through direct commits to unprotected branches or through pull requests from forks. The malicious code runs as soon as the pipeline is triggered by the push or pull request event.
Indirect PPE (I-PPE): When the direct modification of CI configurations is prevented, the attacker injects malicious code into files referenced by the pipeline: scripts, makefiles, build tools, or test dependencies.
The security advisory GHSA-vvj3-c3rp-c85p published for PHPUnit in January 2026 is an example of this category: a manipulated .coverage file represents an attack on a file that the pipeline processes during test execution.
This shows even more clearly in security advisory GHSA-qrr6-mg7r-m243, published in April 2026: through a phpunit.xml manipulated in a pull request, additional PHP INI directives such as auto_prepend_file can be injected into child processes. In a code review such a change looks harmless, yet it enables the execution of malicious code.
Public PPE (3PE): This variant targets public repositories that automatically run code from pull requests from forks without any approval being required. Organisations that run CI/CD pipelines on untrusted code from external contributors are the most exposed to this attack vector.
When theory becomes practice
What this means in the worst case is shown by a series of prominent incidents. Not every one of them is a PPE in the strict sense, but each one demonstrates what the compromise of a build or CI/CD environment can do.
In the SolarWinds attack (2020), state-backed attackers compromised SolarWinds' build infrastructure and planted the SUNBURST malware in software updates that were distributed to roughly 18,000 customers. The attackers operated undetected for six months. Those affected included several US government agencies.
In the PyTorch CI/CD compromise (2023-2024), security researchers discovered that the PyTorch repository on GitHub, using self-hosted runners, was vulnerable to a new class of PPE attacks. By submitting a simple typo correction via a fork pull request, the researchers became contributors and were able to execute arbitrary code on PyTorch's infrastructure. They extracted AWS credentials and GitHub personal access tokens with administrator access to 93 repositories.
In 2025, researchers identified critical PPE vulnerabilities in repositories belonging to Microsoft and other Fortune 500 companies. Misconfigured workflows that used the pull_request_target trigger allowed attackers to execute code in privileged contexts, exfiltrate secrets, and push malicious code to trusted branches.
One common denominator stands out here: if we trace back nearly every supply chain incident in open source projects over the past year and a half, we end up at a YAML file in .github/workflows, as Andrew Nesbitt sets out in GitHub Actions is the weakest link. The compromise of tj-actions affected 23,000 downstream repositories, the attack on nx exposed more than 5,000 private repositories, and in the case of Elementary Data ten minutes were enough between a malicious comment and the compromise.
Reduction as defence
The usual response to such threats is to add more layers of protection. But there is a more effective question than “How do we protect this branch?”. That question is: do we need this branch at all?
A branch that does not exist cannot receive a pull request carrying a PPE attack. A CI/CD pipeline will not run a poisoned workflow against a branch that never existed. This is not minimalism for its own sake, but the simplest way to reduce the attack surface.
Review the branches in your repositories. Ask yourself: does this branch serve a purpose today, or is it a leftover from a past sprint, a forgotten experiment, a “we might still need this”? If it is not actively needed, remove it. What does not exist cannot be attacked.
Protect what must exist. Remove what need not exist. The safest door is the one that was never built.
Defence in depth
Reduction alone is not enough. For everything that must exist, we need defence in depth: several independent security layers that provide resilience when a single layer fails.
Isolated execution environments
Ephemeral runners are among the most effective countermeasures against PPE. These short-lived instances are created for individual CI/CD jobs and destroyed immediately after completion. Their short lifespan reduces the window for exploitation, prevents persistent access, and ensures that every job starts in a clean environment.
Code review and branch protection
Human review remains one of the most effective lines of defence against malicious code injection. Branch protection rules should require approval by named reviewers before code from pull requests is run, prohibit direct pushes to protected branches that contain CI/CD configurations, and enforce status checks before a merge.
GitHub's pull_request_target trigger, which grants fork pull requests elevated permissions, should only be used with explicit safeguards. The safer pull_request trigger runs with restricted permissions in the context of the forked branch.
Secrets management and least privilege
Modern CI/CD systems should use time-limited per-run identities instead of long-lived credentials. OIDC allows credentials to be generated dynamically without storing them. Every pipeline should access only the credentials required for its specific purpose, with the minimum necessary privileges. This limits the damage when a pipeline is compromised.
Monitoring and detection
Security visibility must extend into CI/CD pipelines. This includes the monitoring of process execution, network connections, and file operations within runners. Outbound network connections should be monitored and restricted to prevent data exfiltration. Comprehensive audit logs of all pipeline activity are just as indispensable as the detection of anomalies: unexpected access to secrets, modified artefacts, or unusual execution times.
Shared responsibility
Security is not the sole task of those who build a tool. Whoever uses it carries a share of the responsibility for security as well. For the CI/CD pipeline, PHPUnit's security policy draws this line explicitly:
Protecting the pipeline is the responsibility of the operator of the pipeline: limit which events trigger workflows, require review before workflows run on contributions from outside collaborators, isolate jobs that run untrusted code, and do not expose secrets to such jobs.
Tool maintainers carry the responsibility for a secure implementation within their sphere of influence. They should fix vulnerabilities, provide clear documentation about operational boundaries, and implement safeguards where it is feasible.
But tool maintainers cannot secure what they do not control. They cannot enforce code review policies in your organisation, isolate your CI/CD runners, or set up least-privilege access controls for your pipelines. And they cannot stop you from deploying development tools in production environments.
Whoever acknowledges these boundaries can build defence in depth across them. Use ephemeral runners to limit exposure. Require code reviews before untrusted code is run. Implement secrets management with time-limited credentials. Monitor pipeline activity for anomalies. Never deploy development tools in the production environment.
And above all: remove branches that are no longer needed. Because the safest door is the one that was never built.