OWASP Top 10 CI/CD Security Risks
Source: OWASP Top 10 CI/CD Security Risks (2022, current as of 2026) Reference: https://owasp.org/www-project-top-10-ci-cd-security-risks/
CICD-SEC-1: Insufficient Flow Control Mechanisms
What it isNo required approvals, branch protections, or merge controls. Any developer (or compromised account) can push directly to production branches or approve their own changes.
Attack scenarioDeveloper account is phished → attacker pushes malicious code directly to main → deploys to production without review.
Controls
- Enforce branch protection rules (required reviewers, status checks)
- Require 2+ approvals for production-branch merges
- Prevent self-approval
- Sign commits (GPG/SSH); require signed commits on protected branches
CICD-SEC-2: Inadequate Identity and Access Management
What it isOverly permissive service accounts, stale credentials, no least-privilege on pipeline identities, no MFA on SCM accounts.
Attack scenarioBuild service account has Owner role on GCP project → compromise of pipeline = full GCP takeover.
Controls
- Principle of least privilege for all pipeline identities
- Use short-lived OIDC tokens instead of long-lived keys
- Audit and rotate service account keys regularly
- Enforce MFA on all developer accounts accessing the SCM
- Review and remove stale machine accounts
CICD-SEC-3: Dependency Chain Abuse
What it isAttackers inject malicious code into open-source packages, internal package registries, or build tool plugins that the pipeline installs.
Sub-types
public package shadows private internal package
reqeusts, colu, color-me-js
legitimate package taken over (e.g., event-stream incident)
contributor adds malicious code to a dependency
Controls
- Lock files + hash verification (
requirements.txtwith hashes,package-lock.json) - Use a private artifact registry (Artifactory, Nexus, CodeArtifact) with allow-listing
- SCA scanning on every build (OWASP Dependency-Check, Snyk,
pip-audit) - VCS namespace protection for internal package names
CICD-SEC-4: Poisoned Pipeline Execution (PPE)
What it isAttacker modifies pipeline configuration or the code it executes (scripts, Makefiles) to run arbitrary commands within the CI environment. Most dangerous with pull_request_target or workflows triggered by forks.
Memory hookPPE = "the build server runs attacker code with your secrets." The CI runner is a juicy target because it holds cloud credentials, signing keys, and deploy access, and its whole job is to execute code from the repo. Poisoned Pipeline Execution abuses that: get the runner to run something you control — the workflow YAML (direct), a script/Makefile it calls (indirect), or a fork's PR via
pull_request_target(public) — and it executes with the pipeline's privileges. The killer fact aboutpull_request_target: it runs with write permissions and secrets but checks out untrusted fork code — so checking out and running that code hands a stranger your secrets. Mnemonic: PPE turns "run my tests" into "run my exfiltration, signed by your CI."
Types
| Type | Attacker Controls |
|---|---|
| Direct PPE | Workflow YAML itself |
| Indirect PPE | Script/Makefile called by workflow |
| Public PPE | Open-source repo with public PR triggers |
Attack path
- Fork public repo
- Modify
.github/workflows/ci.ymlOR modifyMakefile - Open PR →
pull_request_targetruns attacker code with repo secrets
Controls
- Never use
pull_request_targetwith untrusted checkout - Use
pull_request(no secrets) for untrusted forks - Require approval for first-time contributors
- Pin third-party actions to commit SHAs
CICD-SEC-5: Insufficient PBAC (Pipeline-Based Access Controls)
What it isThe pipeline identity (build node, runner) has access to more systems, secrets, and infrastructure than the pipeline's actual job requires.
ScenarioA runner building a front-end microservice has access to production database credentials for unrelated services stored in the same secrets store.
Controls
- Scope secrets to specific pipelines, environments, or stages
- Use GitHub Environments with required reviewers for production
- Separate runners per environment tier (dev/staging/prod)
- Audit what each pipeline can access
CICD-SEC-6: Insufficient Credential Hygiene
What it isSecrets are stored, transmitted, or logged in insecure ways — hard-coded in code, printed in build logs, stored in plain text, or never rotated.
Common patterns
# Hard-coded (found via truffleHog / gitleaks)
API_KEY = "AKIAIOSFODNN7EXAMPLE"
# Leaked in logs
print(f"Connecting with token: {os.environ['DB_PASSWORD']}")
# In Dockerfile
ENV DB_PASSWORD=supersecretControls
- Pre-commit hooks:
gitleaks,detect-secrets - GitHub Secret Scanning + push protection
- Never echo/print secrets in pipeline steps
- Dynamic secrets with short TTLs (Vault, cloud secret managers)
- Regular audit of all stored secrets
CICD-SEC-7: Insecure System Configuration
What it isThe CI/CD system itself (Jenkins, GitHub Actions runner, ArgoCD, etc.) is misconfigured — default credentials, exposed admin UIs, no auth on build artifacts, etc.
Examples
- Jenkins with no auth, exposed on public internet
- ArgoCD default admin password not changed
- Nexus artifact registry with anonymous read + write
- Build agents with world-readable workspace directories
Controls
- Harden CI/CD system per CIS benchmarks
- No admin UIs exposed to the internet without VPN/Zero Trust
- Ephemeral build runners (no persistent state)
- Audit build artifact access controls
CICD-SEC-8: Ungoverned Usage of 3rd Party Services
What it isUnreviewed third-party actions, plugins, or integrations are added to the pipeline, creating implicit trust in external code.
ScenarioDeveloper adds uses: random-action@v2 → action author publishes new version with credential-stealing code → runs in all pipelines using that action.
Memory hook"a tag is a sticky note; a SHA is a fingerprint."
uses: some-action@v2trusts whatever the author currently pointsv2at — they (or whoever compromises them) can silently move that tag to malicious code, and it runs in your pipeline with your secrets. Pinning to a full commit SHA (@a1b2c3...) freezes the exact code; it can't be swapped under you. This is exactly the tj-actions/changed-files compromise (2025), where a popular action's tags were repointed to credential-stealing code and ran in thousands of pipelines. Mnemonic: pin to the fingerprint, not the sticky note — and ideally fork-and-host critical actions.
Controls
- Maintain an approved action allow-list
- Pin to commit SHA (not tags)
- Fork and host approved actions internally
- Review action code before first use
- Dependabot for action version monitoring
CICD-SEC-9: Improper Artifact Integrity Validation
What it isBuild artifacts (containers, binaries, packages) are not signed, and consumers don't verify signatures before deploying. Attacker can swap a legitimate artifact for a malicious one.
Attack pathAttacker gains write access to artifact registry → replaces image with backdoored version → pipeline deploys malicious image.
Controls
- Sign artifacts with Sigstore/cosign at build time
- Enforce signature verification at deploy time (Cosign + Kyverno policy, Binary Authorization on GKE)
- Use immutable artifact registries (OCI digest pinning)
- SLSA provenance attestation
CICD-SEC-10: Insufficient Logging and Visibility
What it isCI/CD activities (who ran what pipeline, what secrets were accessed, what was deployed) are not logged or are not monitored.
ImpactAttacker can exfiltrate secrets, modify pipeline, or deploy malicious code — and it goes undetected.
Controls
- Audit logs for: pipeline runs, secrets access, permission changes, admin actions
- Alert on: first-time contributors triggering privileged workflows, secrets accessed outside expected hours, unusual build times
- Centralise CI/CD logs into SIEM (Splunk, Chronicle)
- Retain logs for ≥90 days
Attack Chain: Full CI/CD Compromise
1. Recon → discover SCM platform, CI system, artifact registry
2. Initial Access → phish developer OR exploit misconfigured CI (CICD-SEC-7)
3. PPE → modify workflow/script to exfiltrate secrets (CICD-SEC-4)
4. Credential Theft → dump GITHUB_TOKEN, cloud keys, deploy keys (CICD-SEC-6)
5. Lateral Move → use CI identity to access prod environment (CICD-SEC-5)
6. Persistence → backdoor dependency or action (CICD-SEC-3, CICD-SEC-8)
7. Cover tracks → no logging to notice (CICD-SEC-10)Interview Questions: OWASP CI/CD
I'd argue Poisoned Pipeline Execution combined with weak pipeline access controls, because the CI runner is the crown jewel — it holds cloud credentials, signing keys, and production deploy access, and its job is literally to execute code from the repo. If an attacker can make it run their code, especially via pull_request_target checking out untrusted fork code with secrets, they get those privileges and can pivot straight to production or backdoor the artifacts everyone trusts. That last part is what makes CI/CD compromise so severe — it's a supply-chain position: one poisoned build can ship malware to every downstream consumer with a valid signature, as SolarWinds showed. So the blast radius is the whole software delivery chain, not one app.
Both get the CI runner to execute attacker-controlled code, but differ in what the attacker edits. Direct PPE is modifying the pipeline definition itself — the workflow YAML — for example adding a step that dumps secrets, which works if an attacker can push to a branch that triggers the pipeline. Indirect PPE is modifying something the pipeline calls rather than the pipeline file — a Makefile, a build script, a test config, an npm prebuild hook — so even if the workflow YAML is protected, the code it executes isn't. Indirect is sneakier because reviewers focus on the workflow file and overlook the scripts it invokes. The public variant is opening a fork PR that triggers pull_request_target, running your changes with the upstream repo's secrets.
Dependency confusion exploits how package managers resolve names across public and private registries. If a company uses an internal package named, say, acme-utils, and an attacker publishes a package with the same name on the public registry with a higher version number, the build tool may pull the attacker's public package instead of the internal one — because it picks the highest version and the public registry is in the search path. The malicious package then runs install-time code in the build. Post-compromise detection: look for installs of your internal package names resolving from the public registry, unexpected outbound network connections during dependency installation, and new public packages matching your internal namespace. Prevention is scoping/namespacing internal packages, configuring the resolver to prefer the private registry, and pinning with hashes.
First, scope what that token could do — GITHUB_TOKEN permissions are per-workflow, so I check whether it had write to the repo, packages, or deployments. The default token is short-lived and expires at job end, which limits exposure, but if it was a PAT or an app token that's far worse. I revoke or rotate it immediately and invalidate any sessions. Then I scope using audit logs: did anything use that token between leak and revocation — pushes, releases, package publishes, workflow changes, new collaborators or deploy keys. I look specifically for persistence the attacker may have planted. I purge the secret from logs and history, fix the step that printed it — never echo secrets — and add push protection and log scanning. Finally I tighten the workflow's token permissions to least privilege so the next leak is less useful.
The risk is that someone swaps a legitimate build artifact for a backdoored one and consumers deploy it unknowingly. Sigstore/cosign lets the pipeline cryptographically sign artifacts at build time and lets consumers verify those signatures before deploy — so an unsigned or tampered image is rejected, enforced at admission via something like Kyverno or Binary Authorization. SLSA provenance goes further: it produces a signed attestation describing how the artifact was built — which source commit, which builder, which steps — so you can verify not just that it's signed but that it came from your trusted pipeline and wasn't built somewhere else. Together they close the gap by making "where did this artifact come from and was it altered" cryptographically verifiable, rather than trusting whatever happens to be in the registry.
I'd centralize CI/CD audit logs into the SIEM and cover pipeline runs, secret access, permission and branch-protection changes, and admin actions on the CI system and SCM. Then build detections for the suspicious patterns: a first-time or external contributor triggering a privileged workflow, secrets accessed outside expected hours or by an unexpected pipeline, workflow files or self-hosted runners being modified, new deploy keys or collaborators, unusual outbound network connections from a runner, and builds that take abnormally long or produce unexpected artifacts. I'd also watch for new public packages matching our internal namespace and for actions whose pinned SHA changed. The goal is visibility into both the control plane — who changed what — and the runtime — what the pipeline actually did — since attackers rely on CI/CD being a logging blind spot.