## Summary

CVE-2026-84645 (Jenkins SECURITY-3972) is an authenticated remote code execution vulnerability caused by Jenkins XStream deserialization accepting implementations of `hudson.model.PersistenceRoot` in nested positions of attacker-submitted configuration object graphs. The accepted nested objects remain reflectively traversable by Stapler. In this run, a user limited to Overall/Read, Item/Read, and Item/Configure submitted a job `config.xml` containing `SCMTrigger.BuildAction -> FreeStyleBuild -> FreeStyleProject -> hudson.model.Hudson`, where the forged `Hudson` carried the core `AuthorizationStrategy$Unsecured`; the attacker then reached that object's `doScriptText` method through the forged Stapler route and executed Groovy plus the controller-local `id` command.

## Impact

- **Affected package/component:** Jenkins core XStream handling in `hudson.util.RobustReflectionConverter`, combined with Stapler routing over Jenkins model objects.
- **Affected versions:** Jenkins weekly 2.579 and earlier, and Jenkins LTS 2.568.2 and earlier, per the official advisory.
- **Fixed versions:** Jenkins weekly 2.580 and LTS 2.568.3.
- **Risk level:** High (official CVSS 3.1 vector `AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H`, score 8.8).
- **Consequences:** An authenticated user with job configuration rights can execute arbitrary Groovy and operating-system commands in the Jenkins controller JVM/container security context, enabling full compromise of controller data and behavior.

The runtime used the real Jenkins HTTP/API boundary. Matrix Authorization Strategy Plugin 3.3 was installed only to express the claim's low-privilege account precisely. Before exploitation, the same `attacker` account received HTTP 403 from the legitimate root `/scriptText` endpoint in every vulnerable and fixed attempt.

## Impact Parity

- **Disclosed/claimed maximum impact:** Authenticated remote code execution on the Jenkins controller.
- **Reproduced impact from this run:** Authenticated remote code execution on two independent Jenkins 2.579 controller processes. Each attacker-supplied Groovy script created a unique controller-local marker and executed `id`, returning `uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)`.
- **Parity:** `full`.
- **Not demonstrated:** No claimed impact remains unproven. The proof intentionally stops after a harmless unique file write and `id`; it does not perform persistence, secret extraction, or destructive actions.

## Root Cause

`PersistenceRoot` identifies Jenkins model objects whose state belongs in an independent top-level persistence document, such as a Jenkins/Hudson singleton, item/job, node, or build. Before the fix, `RobustReflectionConverter` applied the JEP-200 class allowlist but did not enforce the structural invariant that newly deserialized `PersistenceRoot` instances must not occur as ordinary nested field values. Consequently, a class could be allowed by identity yet unsafe in its graph position.

The exploit uses only Jenkins core types:

1. `hudson.triggers.SCMTrigger$BuildAction` is inserted into the carrier job's persistent `actions` list. It exposes the Stapler URL name `pollingLog` and a public `getRun()` accessor.
2. Its private `run` field is deserialized as a nested `hudson.model.FreeStyleBuild` (a `PersistenceRoot`).
3. The build's `project` field is deserialized as a nested `hudson.model.FreeStyleProject` (also a `PersistenceRoot`).
4. The nested project's `parent` field is deserialized as a second `hudson.model.Hudson`/`jenkins.model.Jenkins` singleton object (also a `PersistenceRoot`).
5. That forged root object carries `hudson.security.AuthorizationStrategy$Unsecured`, so `Hudson#doScriptText` calls `Jenkins._doScript(..., getACL())` with an ACL that allows `ADMINISTER`.
6. Stapler reflectively traverses `/job/carrier/pollingLog/run/project/parent/scriptText` and invokes the forged object's Script Console endpoint, despite the authenticated principal lacking real Jenkins `ADMINISTER` permission.

The primary fixing commit is [`0d731367e08656f8cd1e8275f0e820f97af07fc6`](https://github.com/jenkinsci/jenkins/commit/0d731367e08656f8cd1e8275f0e820f97af07fc6) (`[SECURITY-3972]`), which is present in tag `jenkins-2.580` and absent from `jenkins-2.579`. It adds a `PersistenceRoot` check in `RobustReflectionConverter` and throws `CriticalXStreamException` for unsafe nested instances, with narrow exceptions for references, replacement placeholders, and registered single-value converters. It also adds a second-Jenkins-instance guard and safe replacer logic in `Jenkins`. Related hardening annotations in the 2.580 release prevent unsafe transient-field reconstruction.

In the fixed runtime, the same `config.xml` POST completes but serializing the job back shows only `<hudson.triggers.SCMTrigger_-BuildAction/>`; the forbidden nested Run/Job/Hudson graph is absent. The forged route returns HTTP 404 and no marker is created.

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` from any directory. `PRUVA_ROOT` may optionally identify the bundle root.
2. The script reads `bundle/project_cache_context.json`, reuses the prepared cache when available, and otherwise uses bundle-owned fallback paths.
3. It downloads three pinned plugin dependencies only when absent and verifies their SHA-256 values: Matrix Authorization Strategy 3.3, Ionicons API `94.vcc3065403257`, and commons-lang3 API `3.18.0-98.v3a_674c06072d`.
4. It pulls Jenkins by immutable image digest: vulnerable 2.579 (`sha256:a7342867…d7412be`) and fixed 2.580 (`sha256:0e50a5b1…0839b1`).
5. For each of two vulnerable and two fixed clean controller processes, it provisions `admin` and a low-privilege `attacker`, creates the `carrier` freestyle job, verifies that direct Script Console access returns 403, submits the crafted XML over authenticated HTTP, and posts unique Groovy to the forged route.
6. Vulnerable success requires HTTP 200, the unique marker in both the HTTP response and controller-local marker file, and `uid=1000(jenkins)` output. Fixed success requires a non-success forged-route status, marker absence, and absence of the nested graph from the resulting job XML.
7. The script finalizes controller logs, writes `bundle/repro/runtime_manifest.json` with SHA-256 bindings for all immutable proof artifacts, prints `CONFIRMED`, and exits 0 only if every assertion passes.

Expected terminal output:

```text
CONFIRMED: SECURITY-3972 achieved authenticated remote command execution on two Jenkins 2.579 controllers; two Jenkins 2.580 controls failed closed.
```

## Evidence

Primary current-run evidence is under `bundle/repro/proof/`, and every file is bound in `bundle/repro/runtime_manifest.json`:

- `vulnerable_1.route.response.body` and `vulnerable_2.route.response.body` contain unique markers and controller command output. Latest run excerpts:

  ```text
  Result: {marker=CVE_2026_84645_vulnerable_1_12521_12184, id=uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)}
  Result: {marker=CVE_2026_84645_vulnerable_2_12521_18651, id=uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)}
  ```

- `vulnerable_1.marker.txt` and `vulnerable_2.marker.txt` are controller-local command markers whose bytes match the per-process values in `vulnerable_1.capability_observation.json` and `vulnerable_2.capability_observation.json`.
- `vulnerable_{1,2}.direct_console.headers` begin with `HTTP/1.1 403 Forbidden`, proving the attacker did not already have Script Console access.
- `vulnerable_{1,2}.config.request.txt` record the redacted authenticated API request and exact nested graph.
- `vulnerable_{1,2}.route.request.txt` record the forged Stapler route and bounded Groovy effect.
- `fixed_{1,2}.route.response.headers` begin with `HTTP/1.1 404 Not Found`.
- `fixed_{1,2}.marker_absent.txt` and `fixed_{1,2}.negative_control.json` record that the corresponding unique marker was not created after the same procedure reached the fixed target.
- `fixed_{1,2}.config.after.xml` contain the empty `SCMTrigger_-BuildAction` and no nested `FreeStyleBuild`.
- `target_identity.txt` records the immutable image digests/image IDs, source tag commits, platform, and architecture.
- `vulnerable_{1,2}.service.log` and `fixed_{1,2}.service.log` show real Jenkins 2.579/2.580 startup and production service initialization.
- `bundle/logs/reproduction_steps.log` and `bundle/logs/reproduction_steps_second.log` show two consecutive successful executions of the final script.

Environment identity:

- Vulnerable source tag commit: `9095ea3a5c5e7dcd392695a5dd880af1c9910ddf` (`jenkins-2.579`).
- Fixed source tag commit: `497de4961ad80d97e26bfdeb0d2e40442a84ecb0` (`jenkins-2.580`).
- Vulnerable image digest: `sha256:a7342867ea33efaacf825229d50b7fc77c144ecada9719ab4e32419f5d7412be`.
- Fixed image digest: `sha256:0e50a5b11ac14f3b84e529d725ed3a1c4b17ba16188dfa8d9a0189428b0839b1`.
- Runtime platform: Linux x86-64/amd64, Docker, Jenkins bundled JVM 21.
- Sanitizers: None.

## Recommendations / Next Steps

- Upgrade Jenkins weekly to 2.580 or later, or Jenkins LTS to 2.568.3 or later.
- Preserve the fixed `PersistenceRoot` structural check and its safe-reference exceptions; do not rely on class allowlisting alone for objects with graph-position invariants.
- Retain the second-singleton guard in `Jenkins#readResolve` as defense in depth.
- Review plugin-defined `PersistenceRoot` implementations and plugin actions that expose routable accessors to root objects.
- Add regression tests for all configuration-accepting endpoints, including jobs, nodes, builds, users, views, and plugin-defined XML documents. Tests should verify both deserialization rejection/neutralization and that Stapler cannot traverse any partially retained graph.
- Independently test safe back-references, `writeReplace`/`readResolve` placeholders, and single-value converters to avoid compatibility regressions without weakening the structural policy.

## Additional Notes

- **Idempotency:** Confirmed. The final `bundle/repro/reproduction_steps.sh` passed twice consecutively. Each execution created two new vulnerable and two new fixed Jenkins controller processes with unique container IDs and markers.
- **Privileges:** The `attacker` account has only Overall/Read, Item/Read, and Item/Configure. A direct `/scriptText` request is a mandatory 403 negative precondition in all four attempts.
- **Safety:** The only command effects are a uniquely named file under the ephemeral controller's `/tmp` and `id`; all test containers are removed on completion or interruption.
- **Fixed behavior nuance:** The 2.580 endpoint returned HTTP 200 for the configuration update while omitting the prohibited nested values. Security parity is established by the non-routable graph, HTTP 404 forged route, and marker absence rather than by requiring the initial XML POST itself to return an error.
- **Network dependency:** A cold run needs access to the pinned Jenkins images and plugin URLs. A warm prepared cache reuses exact plugin bytes, and Docker reuses digest-addressed images.
