# Root Cause Analysis: CVE-2026-84647 (Jenkins SECURITY-3915)

## Summary

Jenkins 2.579 bundles Stapler 2107.v8dfcb_e8ed317, whose structured-form collection binder trusts every class name encoded as a key beneath `stapler-class-bag`. It loads and instantiates the requested class without first checking that the class is a subtype of the collection's declared item type. An authenticated user with only Overall/Read can reach the personal My Views creation endpoint and submit a `ListView.columns` collection that names an unrelated configuration class. In this run, the attacker selected `io.jenkins.plugins.file_parameters.StashedFileParameterValue`; its data-bound constructor consumed an uploaded file and an attacker-controlled traversal name, placing attacker Groovy under `JENKINS_HOME/init.groovy.d`. On an ordinary Jenkins restart, the real controller executed the Groovy startup hook and created a unique command-execution marker. Jenkins 2.580 reached the same endpoint but ignored the incompatible class before construction.

## Impact

- **Affected component:** Jenkins core's bundled Stapler structured form-binding implementation, specifically collection conversion in `org.kohsuke.stapler.RequestImpl.TypePair.convertJSON`.
- **Affected versions:** Stapler 2107.v8dfcb_e8ed317 and earlier, except 2088.2093.vd7c3e58008a_6; included in Jenkins weekly 2.579 and earlier and Jenkins LTS 2.568.2 and earlier. The issue is fixed in Jenkins weekly 2.580 and LTS 2.568.3.
- **Tested vulnerable target:** Immutable image `jenkins/jenkins@sha256:a7342867ea33efaacf825229d50b7fc77c144ecada9719ab4e32419f5d7412be` (Jenkins 2.579, source commit `9095ea3a5c5e7dcd392695a5dd880af1c9910ddf`).
- **Tested fixed target:** Immutable image `jenkins/jenkins@sha256:0e50a5b11ac14f3b84e529d725ed3a1c4b17ba16188dfa8d9a0189428b0839b1` (Jenkins 2.580, source commit `497de4961ad80d97e26bfdeb0d2e40442a84ecb0`).
- **Risk:** High. A network attacker with only Overall/Read can invoke constructors and setters of configuration-related classes outside the intended destination type. Impact depends on available data-bound gadget classes. With File Parameter Plugin 425.v3fa_801681b_5e, the run demonstrated a controller file-write gadget and persistent controller-side code execution after restart. The low-privilege user was denied direct Script Console access.

## Impact Parity

- **Disclosed/claimed maximum impact:** Remote code execution on the Jenkins controller.
- **Reproduced impact from this run:** Full controller code execution. Two independent low-privilege HTTP submissions placed attacker-selected Groovy in `JENKINS_HOME/init.groovy.d`; after restarting each controller, Jenkins' real `GroovyHookScript` executed it and created a unique marker.
- **Parity:** `full`.
- **Not demonstrated:** Immediate, restart-free command execution was not required or demonstrated. The proven chain requires a later controller restart and the installed File Parameter Plugin 425 gadget. No sanitizer, direct unit harness, administrative token, Script Console, or Item/Configure permission was used for the attack.

## Root Cause

For collection-valued form fields, Stapler recognizes a JSON object containing `"stapler-class-bag": true`. In vulnerable `RequestImpl.TypePair.convertJSON`, it iterates over all object entries, converts each entry key from JSON-safe `-` escaping back to `.`, and performs:

```java
Class<?> itemType = cl.loadClass(className);
l.add(bindJSON(itemType, (JSONObject) v));
```

Although the collection lister carries the expected element class as `l.itemType`, the vulnerable code never checks `l.itemType.isAssignableFrom(itemType)` before recursively binding and constructing the attacker-selected class. Type incompatibility is discovered only later, if at all; constructor and setter side effects have already occurred. The low-privilege personal-view endpoint is exploitable because creating a personal view is permitted with Overall/Read and `ListView` has a data-bound `List<ListViewColumn>` property.

The security fix is Stapler commit [`b49b34c07103fb238a566e51b531729f1b68e73d`](https://github.com/jenkinsci/stapler/commit/b49b34c07103fb238a566e51b531729f1b68e73d) (`SECURITY-3915`). It changes class loading to:

```java
Class<?> itemType = cl.loadClass(className).asSubclass(l.itemType);
```

and catches `ClassCastException` together with `ClassNotFoundException`. Consequently, Jenkins 2.580 ignores `StashedFileParameterValue` when binding `ListView.columns` because it is not a `ListViewColumn`, so its constructor never receives the upload and cannot create the startup hook.

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` with Bash and a working Docker daemon.
2. The script reads `bundle/project_cache_context.json` when available, otherwise uses `bundle/artifacts/jenkins-security-3915`; downloads checksum-pinned plugins; and uses immutable Jenkins 2.579 and 2.580 image digests.
3. For each target, it starts clean controller processes with matrix authorization granting `alice` only Overall/Read. It proves the restriction with `GET /script` returning HTTP 403.
4. It sends an authenticated multipart `POST /user/alice/my-views/createView`. The submitted `ListView.columns.stapler-class-bag` names `StashedFileParameterValue`, and the uploaded file is attacker Groovy whose constructor argument places it at `../../init.groovy.d/cve84647.groovy` relative to the gadget's temporary directory.
5. It restarts the real Jenkins controller and checks for a unique target-local marker written by that Groovy.
6. The script performs two clean Jenkins 2.579 attempts and two clean Jenkins 2.580 controls, emits `bundle/repro/runtime_manifest.json`, verifies every bound artifact hash, and exits 0 only for the expected vulnerable/fixed divergence.
7. Expected terminal output includes:

```text
CONFIRMED vulnerable attempt 1: low-privilege request executed attacker Groovy after restart
CONFIRMED vulnerable attempt 2: low-privilege request executed attacker Groovy after restart
CONFIRMED fixed attempt 1: target path reached but incompatible class constructor did not run
CONFIRMED fixed attempt 2: target path reached but incompatible class constructor did not run
CVE-2026-84647 CONFIRMED: two low-privilege production HTTP attempts achieved controller code execution; two fixed controls failed closed.
```

## Evidence

- `bundle/repro/runtime_manifest.json`: strict runtime manifest with 42 immutable proof artifacts and SHA-256 bindings.
- `bundle/repro/proof/vulnerable_1.request.json` and `vulnerable_2.request.json`: attacker request shape crossing the actual Jenkins HTTP endpoint.
- `bundle/repro/proof/vulnerable_1.response.headers` and `vulnerable_2.response.headers`: endpoint responses.
- `bundle/repro/proof/vulnerable_1.privilege.txt` and `vulnerable_2.privilege.txt`: `GET /script HTTP 403`, proving the attacker did not have Overall/Administer.
- `bundle/repro/proof/vulnerable_1.result.txt` and `vulnerable_2.result.txt`: each records `payload_script_present=true` and `marker_present=true`.
- `bundle/repro/proof/vulnerable_1.marker.txt` and `vulnerable_2.marker.txt`: unique controller-local command markers.
- `bundle/repro/proof/vulnerable_1.service.log` and `vulnerable_2.service.log`: real Jenkins lifecycle logs. Each includes `Starting version 2.579` and `Executing /var/jenkins_home/init.groovy.d/cve84647.groovy` after restart.
- `bundle/repro/proof/fixed_1.result.txt` and `fixed_2.result.txt`: same endpoint reached on Jenkins 2.580, but `payload_script_present=false` and `marker_present=false`.
- `bundle/repro/proof/fixed_1.negative_control.json` and `fixed_2.negative_control.json`: explicit target-reached fixed observations.
- `bundle/repro/proof/fixed_1.service.log` and `fixed_2.service.log`: Jenkins 2.580 starts and restarts without executing the attacker hook.
- `bundle/logs/reproduction_steps.log`: concise final-run diagnostic summary.

A final vulnerable log excerpt is:

```text
Starting version 2.579
Executing /var/jenkins_home/init.groovy.d/cve84647.groovy
Jenkins is fully up and running
```

The corresponding fixed result is:

```text
POST /user/alice/my-views/createView HTTP 302
payload_script_present=false
marker_present=false
```

The final script was executed twice consecutively. Both complete runs exited 0 (approximately 63 seconds and 58 seconds) and reproduced two vulnerable executions plus two fixed controls on each run.

## Recommendations / Next Steps

- Upgrade Jenkins weekly to 2.580 or newer, or Jenkins LTS to 2.568.3 or newer.
- Ensure the bundled Stapler version contains commit `b49b34c07103fb238a566e51b531729f1b68e73d` or an equivalent backport.
- Independently upgrade plugins fixed in the 2026-09-02 advisory. In particular, update File Parameter Plugin to 433.va_0b_80359d54d or newer; this removes the traversal primitive used in the demonstrated composition.
- Audit `@DataBoundConstructor` and `@DataBoundSetter` methods for side effects and permission checks. Constructors should not write files, initiate network connections, modify global state, or execute commands merely as a consequence of binding.
- Add regression tests for every structured-form collection syntax, especially `stapler-class-bag`, proving that unrelated classes are rejected before constructors or setters execute.
- Monitor `JENKINS_HOME/init.groovy.d`, plugin configuration files, and unexpected controller restarts for evidence of persistence. Rotate secrets if compromise is suspected.

## Additional Notes

- **Idempotency:** Confirmed. The final reproducer passed twice consecutively and uses isolated Docker volumes, unique container names, unique marker values, bounded health checks, and cleanup traps.
- **Runtime boundary:** Production Jenkins HTTP/Stapler endpoint, not a direct parser call or unit harness.
- **Sanitizers:** None used.
- **Exploit preconditions:** Authenticated Overall/Read access; an available configuration-related data-bound gadget (File Parameter Plugin 425 in this proof); write access of the Jenkins process to its own home; and a later controller restart for startup-hook execution.
- **Endpoint behavior:** Both vulnerable and fixed versions return HTTP 302 because the outer personal ListView remains valid. Security is proven by the constructor side effect and marker divergence, not status code alone.
- **Scope limitation:** The demonstrated file gadget comes from an affected plugin. The root vulnerability itself is in Stapler and permits incompatible class selection; other installed classes may provide different consequences.
