Pentesting CI/CD Pipelines: Where Pull Requests Become Production
Cybersecurity
CI/CD is the unguarded path to prod. PR-from-fork secret theft, runner takeover, and artifact substitution still hit Fortune 500 pipelines in 2026. Test it like an attacker would.
By Arjun Raghavan, Security & Systems Lead, BIPI · March 12, 2025 · 9 min read
CI/CD pipelines run with credentials that production never grants to humans. They have cloud admin roles, package registry write tokens, and signing keys. Compromising the pipeline skips every other security control. We pentest CI/CD with the assumption that one developer or one fork PR is enough.
Methodology: the four CI/CD attack paths
Each engagement maps the pipeline against four attack paths and tests every one. First, branch protection bypass: can a contributor merge to main without review? Second, PR-from-fork secret theft: do workflow files run on pull_request_target with secrets exposed? Third, runner takeover: can a malicious workflow command persist on a self-hosted runner? Fourth, artifact substitution: can a forked repo or a deleted maintainer re-use a published artifact name?
- GitHub Actions: search workflows for pull_request_target, workflow_run, and on: push without branch filters.
- Check for actions/checkout with persist-credentials: true (default) on workflows triggered by untrusted events.
- Look for env: ${{ secrets.X }} in steps that run user-controlled input (script: ${{ github.event.pull_request.title }} is RCE).
- GitLab CI: check .gitlab-ci.yml for protected variables exposed to merge request pipelines from forks.
- Inventory self-hosted runners. Are they ephemeral? Is the runner image rebuilt per-job?
PR-from-fork: the easy win
GitHub's pull_request trigger does not expose secrets to workflows triggered by forked PRs. Many repos work around this with pull_request_target, which runs in the context of the base repo with secrets and write permissions. If the workflow then checks out the fork's code (actions/checkout with the PR ref), the attacker's code runs with the base repo's credentials. We have demonstrated this on dozens of high-profile repos. The pattern is that documented.
Self-hosted runner takeover
Self-hosted runners on GitHub default to non-ephemeral. A workflow can write to the filesystem, install a backdoor in /home/runner/.bashrc, and the next job inherits it. If the runner has IAM credentials via instance metadata, those leak too. The fix is ephemeral runners (one job per VM) and runner labels scoped to specific repos.
Artifact substitution and dependency confusion
Internal package names that match public registries (npm, PyPI) get hijacked when the public registry has a higher version. Pipelines that install from a mixed registry without scope locking pull the public version. Test by publishing a benign package with the internal scope name and a higher version, then trigger a build.
Detection
GitHub provides workflow run audit logs and the recent attestations API for build provenance. Alert on workflow file changes (.github/workflows/*) outside of normal change windows. Alert on first-time workflow runs from external contributors. For self-hosted runners, ship runner logs to a SIEM and alert on outbound network connections to non-allowlisted destinations.
Remediation
- Pin every third-party action to a full commit SHA, not a tag. Tags are mutable. Use Dependabot to keep pins current.
- Replace static cloud credentials with OIDC. GitHub Actions and GitLab both issue short-lived tokens to cloud IAM roles.
- Scope GITHUB_TOKEN to the minimum required permissions per workflow with the permissions: key.
- Use ephemeral self-hosted runners. ARC (Actions Runner Controller) on Kubernetes is the standard pattern.
- Disallow pull_request_target with checkout of the PR head. If you must, gate the workflow on a label that requires maintainer review.
- Lock package registries by scope. Configure npm and pip to fail if a private package name is also available publicly.
- Sign artifacts with Sigstore cosign and verify signatures in deployment. Treat unsigned artifacts as untrusted.
- Require code review for changes to .github/workflows/* via CODEOWNERS and branch protection rules.
Read more field notes, explore our services, or get in touch at info@bipi.in. Privacy Policy · Terms.