CI/CD Pipeline Attacks: GitHub Actions, GitLab Runners, and Secret Exfiltration
Cybersecurity
Offensive tradecraft for CI/CD: GitHub Actions injection, GitLab runner abuse, OIDC trust misuse, and exfiltration of build secrets.
By Arjun Raghavan, Security & Systems Lead, BIPI · December 16, 2024 · 12 min read
CI/CD is the part of your infrastructure with the most secrets and the least scrutiny. A build runner has cloud credentials, package registry tokens, and source code, often in a single workflow run. This post covers the attacks that consistently land against GitHub Actions and GitLab CI on real engagements.
Workflow injection in GitHub Actions
The classic primitive is pull_request_target combined with checkout of the PR head. The workflow runs with repository secrets, and an attacker who can open a PR can inject code into the build. Even without pull_request_target, expressions like ${{ github.event.pull_request.title }} embedded in a run step allow command injection because the title is attacker controlled.
Secret exfiltration patterns
- Print environment to a public artifact and download it as the attacker
- Exfil via DNS lookups encoding chunks of secrets in subdomain queries
- Push to an attacker-controlled fork using the repo's PAT or GITHUB_TOKEN
- Open a debug tmate or netcat session for interactive control
OIDC trust misuse
- GitHub Actions OIDC tokens are scoped by repo and ref, but trust policies often accept too much
- Cloud trust policies that allow any branch from a repo make every PR a cloud credential
- Wildcards in the sub claim are the most common misconfiguration
- A compromised maintainer or a malicious PR turns OIDC into root in the cloud account
GitLab runner abuse
Shared runners are convenient and dangerous. A job on a shared runner can sometimes see leftover state from previous jobs, including cached credentials, Docker layers, and clone directories. Group-level runners with elevated tags inherit secrets that are not scoped to the project actually building. Self-hosted runners running as root on a long-lived VM accumulate secrets across hundreds of jobs.
Self-hosted runner takeover
- Persistent runners on private repos can be reused by a malicious workflow you push
- Once code runs on a runner, it can install a backdoor before the runner is decommissioned
- Default runner labels and weak network segmentation let runners reach production systems
- Ephemeral runners with short lifetimes blunt this attack significantly
Your CI is a developer with God-mode access to your cloud, and you let it run any code that lands in a PR.
Detection
- Alert on workflow changes that touch pull_request_target or self-hosted runner labels
- GitHub audit log for OIDC token issuance correlated with cloud STS AssumeRole
- Outbound network rules on runners, denying everything except registry and source pulls
- Secrets in build artifacts caught by repository secret scanning post-publish
Remediation
- Require approval for first-time contributor workflows in GitHub repository settings
- Avoid pull_request_target unless absolutely necessary, and never combine with checkout of PR head
- Scope OIDC trust policies to specific branches, environments, and workflows
- Use ephemeral runners that are destroyed after each job
- Move secrets to a dedicated secret manager and inject only at job time with minimal scope
- Enable branch protection and required reviewers on the .github/workflows directory
Closing
CI is the choke point of modern software supply chains. Treat it like production: review the trust policies, restrict who can change workflow files, and assume every secret it touches is one bad PR away from public. The investment pays for itself the first time an attacker tests it.
Read more field notes, explore our services, or get in touch at info@bipi.in. Privacy Policy · Terms.