Get a quote

Field guide · Source code review

Source code review

Secrets in your pipeline: the risk no one reads.

By Abhimanyu Gupta, Founder & Principal Operator

The most valuable credentials in a codebase often are not in the code at all. They are in the CI configuration nobody reviews, the pipeline that quietly holds the keys to production and runs attacker-influenced input every day. It is the highest-trust, lowest-scrutiny surface most organisations own, and a string of the industry's worst breaches went straight through it.

At a glance

Topic
Secrets and trust in continuous integration and delivery (CI/CD)
Core problem
Highest privilege, lowest review: the pipeline holds production keys and few audit it
Blast radius
One leaked pipeline credential often equals cloud, registry, and deploy access
Real precedents
Codecov (2021), CircleCI (2023), recurring GitHub Actions injection
The fix direction
Short-lived OIDC credentials, least privilege, untrusted-input isolation
Standard
OWASP Top 10 CI/CD Security Risks

The file nobody reviews

Every pull request to the application gets read by a human. The pipeline definition, the YAML in .github/workflows, the CI config, the deploy script, usually does not. It is treated as plumbing: it was written once, it works, and nobody wants to touch it. Yet that file runs with more privilege than almost any application code in the repository. It can read every secret the pipeline is given, reach the cloud account it deploys to, and push artifacts that land in production.

That inversion, maximum privilege and minimum scrutiny, is the whole vulnerability. An attacker who cannot get near your production console can very often get near the pipeline that owns it, and the pipeline hands over what the console would have refused.

Where the secrets actually live. Not only in the config. Pipeline secrets leak through environment variables printed to build logs, cached artifacts, forked-repository builds, over-scoped tokens injected into every job, and third-party actions pulled in by a mutable tag. The credential in plain sight is the least of it.

Why it is worse than a leaked password

A single leaked user password is bad. A single leaked pipeline credential is usually catastrophic, because of what it is connected to:

  • It is high-privilege by default. Deploy credentials can write to production. Registry tokens can publish images the whole fleet will pull. A cloud role assumed by CI often has broad rights nobody trimmed.
  • It is a lateral springboard. From the pipeline, an attacker reaches the cloud account, the artifact registry, the secrets manager, and the source host itself, a hub with spokes into everything that matters.
  • It poisons what ships. Control of the build means control of the artifact. A backdoor inserted in CI is signed by your own process and distributed by your own trust, this is the supply-chain attack in its purest form.
  • It is long-lived and quiet. Static CI tokens are frequently set once and never rotated, and their use looks exactly like a normal build. Detection is hard precisely because the credential is supposed to do powerful things.

It already happened, more than once

This is not hypothetical. Some of the most consequential incidents of recent years were pipeline compromises, and the pattern repeats:

  • Codecov (2021). Attackers modified the widely used Bash Uploader script; it began exfiltrating environment variables, and therefore CI secrets, from thousands of downstream projects. One compromised build tool, an enormous secondary blast radius.
  • CircleCI (2023). A compromised employee laptop led to the theft of customer secrets stored in the CI platform. The vendor's own guidance was blunt: rotate every secret you had stored there. That is the blast radius of “the pipeline holds everything.”
  • GitHub Actions, recurring. Misused workflow triggers and script injection have repeatedly let attacker-controlled pull-request content run in a privileged context and read repository secrets. The technique is well documented and still found in the wild.

The through-line: in each case the attacker did not breach production directly. They breached the thing that builds production, and inherited its trust. Defenders who scope their thinking to “the app” miss the machine that ships the app.

The attack classes we look for

ClassWhat it looks likeResult
Hardcoded secretsKeys and tokens committed to CI config or scriptsDirect credential theft from git history
Secrets in logsEnv vars or debug output printed to build logsExposure to anyone who can read builds
Over-scoped tokensOne broad credential injected into every jobAny compromised step gets everything
Untrusted input in trusted jobsFork PR content running with secretsSecret exfiltration, code execution
Mutable dependenciesThird-party actions pinned to a tag, not a digestSilent supply-chain substitution
Poisoned pipeline executionAttacker edits the build definition itselfBackdoored, self-signed artifacts

The subtle one: untrusted input in a trusted job

The class that catches even careful teams is running attacker-influenced input in a privileged context. A workflow that builds pull requests, and that has access to secrets, is being handed content an outsider wrote. If that content flows unescaped into a shell step, or if the workflow trigger grants secrets to forked contributions, the outsider can run commands inside your pipeline.

reviewer@code-reviewyaml
# the shape of a dangerous step (illustrative, not a recipe):
run: |
  echo "Building PR: ${{ github.event.pull_request.title }}"
  # the PR title is attacker-controlled. If it contains shell
  # metacharacters, it is now running inside a job that can
  # read every secret this workflow was granted.

# and the trigger that makes it exploitable:
on: pull_request_target   # runs with repo secrets, on fork PRs

Neither line is obviously wrong to a reviewer skimming for application bugs, which is exactly why pipeline review is its own discipline. The fix is to treat all external input as tainted (bind it to an environment variable rather than interpolating it into a shell), and to keep secrets out of any job that runs untrusted content at all.

What we check

  • The whole pipeline as attack surface, config, scripts, self-hosted runners, and the cloud roles CI can assume, not just the application it builds.
  • Secret hygiene, scanning current files and git history, checking for secrets reaching logs, and mapping the true scope of every token the pipeline holds.
  • Trigger and input safety, every place attacker-influenced content can reach a privileged job, and whether secrets are exposed to forked contributions.
  • Dependency integrity, whether third-party actions and build tools are pinned to immutable digests, and what would happen if one were compromised upstream.
  • Blast radius, if this one credential leaked, what does it reach, and how quickly could you tell?

Fixing it for good

  • Kill static secrets. Use short-lived, workload-identity credentials (OIDC federation to your cloud) so the pipeline mints a token that expires in minutes instead of holding a key forever.
  • Least privilege, per job. Scope each token to exactly what its step needs; never inject one broad credential into every job.
  • Isolate untrusted input. Do not grant secrets to workflows that run forked or unreviewed content; bind external input to variables, never interpolate it into shell.
  • Pin dependencies to digests. Reference third-party actions and images by immutable hash, not a mutable tag, so upstream cannot be swapped underneath you.
  • Scan and monitor. Run secret scanning in the pipeline, and treat the assumption of a secret leaking as a rotation drill you have actually rehearsed.

The single highest-leverage change is moving from long-lived stored secrets to short-lived OIDC credentials. It shrinks the prize from “a key that works forever” to “a token that expired before the attacker finished reading it.”

What this is, and isn't

  • It is part of a source code review. The pipeline is code, and it is the code with the most privilege, so we read it with the same rigour as the application, and often find more.
  • It is not just secret scanning. A scanner finds a committed key; it does not reason about trigger safety, token scope, or blast radius. Those need a reviewer, the same argument we make in SAST is the floor, not the report.
  • It is fixable without a rebuild. Almost every remediation above is a configuration change, a token scope, a trigger, a credential type, not a re-architecture.

Key takeaway

Your pipeline is the highest-privilege, lowest-scrutiny code you own, and it holds the keys to production while running input strangers wrote. That is why the industry's worst supply-chain breaches went through CI, not through the app.

Read the file nobody reads. Replace long-lived secrets with short-lived ones, scope every token, and keep untrusted input away from privileged jobs. The build system deserves the same review as the thing it builds, because an attacker who owns the builder owns everything it ships.

References & further reading

  1. OWASP, Top 10 CI/CD Security Risks, the reference taxonomy for the classes above.
  2. Codecov, Bash Uploader security update (2021), the compromised build tool and its downstream secret exposure.
  3. CircleCI, January 2023 incident report, the stored-secret theft and the rotate-everything guidance.
  4. GitHub, Preventing pwn requests, on untrusted input and privileged workflow triggers.
  5. GitHub, Security hardening with OpenID Connect, short-lived credentials for pipelines.
  6. OverWatch Labs, SAST is the floor, not the report, on why scanning alone does not cover this.
All posts Source code review

Beyond the blog

Want this tested on you?

Reading about it is one thing. Seeing it proven on your own pipeline is another.