Poisoned Pipeline Execution (PPE) in CI/CD

Tradecraft · 5 min read · Black Hat Brew crew

Poisoned Pipeline Execution (PPE) in CI/CD

Poisoned Pipeline Execution (PPE) in CI/CD: When Your Build System Turns Against You

Executive Summary

Poisoned Pipeline Execution (PPE) is a CI/CD attack where an adversary manipulates code, configuration, or inputs so that malicious commands execute inside a pipeline. The attacker does not need direct access to the CI system. They only need a way to influence what the pipeline runs.

Because CI pipelines often hold deployment keys, cloud credentials, and signing secrets, PPE can escalate quickly from “just a build” to full infrastructure compromise.

What Is Poisoned Pipeline Execution?

Poisoned Pipeline Execution is classified as CICD-SEC-04 in the OWASP Top 10 CI/CD Security Risks. It describes situations where attackers exploit trust relationships between source control and CI systems to inject malicious instructions into automated workflows.

At its core, PPE exploits a simple reality:

Pipelines execute code.

If an attacker controls what gets executed, they control the pipeline.

CI systems are designed to automatically build and test whatever is committed. That automation is power. Power plus misplaced trust equals attack surface.

Why CI Pipelines Are High-Value Targets

CI environments frequently have:

  • Access to environment variables containing secrets

  • Deployment credentials for staging or production

  • Signing keys for release artifacts

  • Permission to publish packages

  • Network access to internal systems

A CI runner often operates with non-interactive, privileged automation tokens. If an attacker achieves code execution inside the pipeline, they inherit that power.

CI is not just do build automation. It also is a privileged control system.

The Core Trust Boundary Problem

In many workflows, especially collaborative or open environments, attackers can influence repository contents — through pull requests, forks, dependency changes, or misconfigured permissions.

The Three Forms of PPE

Security research commonly divides PPE into three conceptual categories.

1. Direct PPE

Direct PPE occurs when the attacker modifies the pipeline configuration itself.

Examples of targets:

  • .github/workflows/*.yml

  • gitlab-ci.yml

  • Jenkinsfile

  • Build configuration scripts

If the pipeline runs automatically on changes to these files without protection or review, malicious commands will execute immediately.

2. Indirect PPE

Even if the CI configuration file is protected, pipelines usually call:

  • Shell scripts

  • Makefiles

  • Test harnesses

  • Package build scripts

  • Custom build tools

If an attacker can modify one of those files, and the pipeline executes it, that is still poisoned execution.

The CI config is clean. The code it runs is not.

Indirect PPE is dangerous because teams often protect pipeline YAML but forget that the YAML references executable content.

3. Public PPE (Third-Party Triggered PPE)

This applies particularly to open-source and collaborative projects.

If pipelines automatically run on external pull requests or forks, attackers can submit changes designed purely to execute malicious commands in CI.

Some CI triggers execute in elevated contexts even when the code comes from untrusted branches. If secrets are injected before trust is validated, exposure becomes trivial.

Automation without gating equals privilege without verification.

Common Misconfigurations That Enable PPE

Several recurring patterns create PPE exposure:

  • Running CI on unreviewed changes
  • Allowing pipeline configuration files to be modified without protection
  • Injecting secrets into jobs triggered by external contributors
  • Executing repository scripts without sandboxing
  • Reusing shared caches or artifacts across trust boundaries
  • Granting overly broad automation tokens

Attack Flow: A Typical PPE Scenario

A simplified sequence looks like this:

  • An attacker gains the ability to submit or modify repository content.

  • They insert malicious commands into pipeline configuration or executed scripts.

  • The CI system automatically runs the pipeline.

  • The malicious code executes in the CI environment.

  • Secrets are exfiltrated or artifacts are modified.

Impact of Successful PPE

The consequences can include:

  • Secret exfiltration (API tokens, cloud keys, signing certificates)
  • Deployment of backdoored artifacts
  • Supply chain compromise of published packages
  • Lateral movement into internal infrastructure
  • Persistent compromise through poisoned build outputs

In mature DevOps environments, CI is deeply integrated into release pipelines. Compromising CI often means compromising production.

Recent CI/CD Poisoned Pipeline Execution Vulnerabilities

1. GHSL-2024-226 & GHSL-2024-227 — PPE & Cache Poisoning in Cilium

Overview:

In the push-chart-ci.yaml workflow used by the Cilium project, a pipeline triggered after successful image builds checked out untrusted code from a pull request head into a job workspace and then executed a script from the repository. This created a PPE condition where attacker-controlled script content could run within the pipeline.

on:
  workflow_run:
    workflows:
      - Image CI Build
      - Hot Fix Image Release Build
    types: [completed]

jobs:
  push-charts:
    runs-on: ubuntu-22.04
    steps:
      - name: Get triggering event ref
        # Loads triggering PR head ref into a variable
      - name: Checkout Source Code
        uses: actions/checkout@v4
        with:
          ref: ${{ steps.get-ref.outputs.ref }}
      - name: Get version
        run: |
          ./contrib/scripts/print-chart-version.sh

Why it matters:

The print-chart-version.sh script was executed after checking out untrusted content, which meant any code in that script (e.g., an echo $(whoami) or malicious payload) ran in the pipeline.

Secondary Issue — Cache Poisoning

This same uncontrolled execution allowed an attacker to modify the default branch CI cache, influencing future workflows that used the cache. Cache poisoning along trust boundaries means files restored for other jobs can contain attacker-controlled content — effectively spreading compromise.

Resources:

Cilium PPE & Cache Poisoning advisory (GHSL-2024-226 / GHSL-2024-227) — GitHub Security Lab

2. GHSL-2024-268 — Hibernate ORM PPE via pull_request_target

Overview:

The Hibernate ORM project’s contributor-build.yml triggered on pull_request_target — a GitHub Actions event that executes workflows with access to secrets for pull requests — but it then checked out the PR head commit instead of the protected base branch. That mixed a privileged context with untrusted code.

on:
  push:
    branches: [main]
  pull_request_target:
    branches: [main]

steps:
  - name: Check out PR head
    uses: actions/checkout@v4
    with:
      ref: "refs/pull/${{ github.event.pull_request.number }}/head"
  - name: Run build script
    env:
      DEVELOCITY_ACCESS_KEY: "${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }}"
    run: |
      .github/ci-prerequisites.sh
      ci/database-start.sh

Why this was dangerous:

Even though the workflow attempted to guard where secrets were passed, checking out the PR head commit placed untrusted scripts into the runner. Those could execute arbitrary code before or while privileged environment variables were loaded, meaning secrets like GRADLE_ENTERPRISE_ACCESS_KEY were exposed in runner memory and thus leakable.

Resources:

GHSL-2024-268 advisory on Hibernate ORM PPE — GitHub Security Lab

3. GHSL-2024-208 — Apache Superset PPE via Issue Comment Trigger

Overview:

In Apache Superset, a workflow triggered by a pull request comment (issue_comment) executed insecure logic that parsed the comment and then checked out the PR head commit — bringing untrusted script files into the pipeline.

on:
  issue_comment:

jobs:
  ephemeral-docker-build:
    runs-on: ubuntu-22.04
    steps:
      - name: Get PR info
        uses: actions/github-script@v7
      - name: Checkout ${{ github.ref }}
        uses: actions/checkout@v4
        with:
          ref: ${{ steps.get-sha.outputs.sha }}
      - name: Build ephemeral env image
        run: |
          ./scripts/build_docker.py ci pull_request --build_context_ref ...

Why this turned into a PPE vector:

  • The workflow didn’t gate who could trigger it on comments.

  • It checked out the PR head commit SHA, which is untrusted code.

  • It then executed a script (build_docker.py) from that untrusted source.

This combination allowed malicious actors to craft PRs and comments to execute arbitrary code with access to secrets like AWS credentials in the CI environment.

Resources:

GHSL-2024-208 advisory on Superset PPE — GitHub Security Lab

REFERNCES