CVE-2026-84650: Verified Reproduction
CVE-2026-84650: Jenkins transient fields deserializable from config.xml → config object overwrite / RCE SECURITY-3972 companion
CVE-2026-84650 is verified against jenkinsci/jenkins · github. Affected versions: Jenkins weekly 2.579 and earlier; Jenkins LTS 2.568.2 and earlier. Vulnerability class: RCE. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00369.
What Is CVE-2026-84650?
CVE-2026-84650 is a high-severity RCE vulnerability affecting jenkinsci/jenkins Jenkins weekly 2.579 and earlier; Jenkins LTS 2.568.2 and earlier.. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00369).
CVE-2026-84650 Severity
CVE-2026-84650 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
Affected jenkinsci/jenkins Versions
jenkinsci/jenkins · github versions Jenkins weekly 2.579 and earlier; Jenkins LTS 2.568.2 and earlier. are affected.
How to Reproduce CVE-2026-84650
pruva-verify REPRO-2026-00369 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00369/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-84650
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
Authenticated Item/Configure config.xml transient object graph followed by attacker-selected Groovy in a forged Stapler route
- POST /job/carrier/config.xml
- POST /job/carrier/pollingLog/run/project/parent/scriptText
How the agent worked
Root Cause and Exploit Chain for CVE-2026-84650
CVE-2026-84650 (Jenkins SECURITY-4032) is an authenticated unsafe-deserialization flaw in Jenkins configuration updates. Jenkins historically excluded Java transient fields from serialization but still populated them while unmarshalling attacker-submitted config.xml. In Jenkins 2.579, an attacker with Item/Configure can therefore create a nested object graph whose transient ownership and identity links are attacker-selected. This run used the real Jenkins HTTP endpoint to set SCMTrigger.BuildAction.run, Run.project, AbstractItem.parent, and AbstractItem.name; Stapler then traversed that forged graph to a nested Jenkins object with an attacker-supplied unsecured authorization strategy, allowing arbitrary Groovy and operating-system command execution on the controller.
- Affected component: Jenkins core, principally
hudson.util.RobustReflectionConverterand security-sensitive transient fields in core model classes. - Affected versions: Jenkins weekly 2.579 and earlier; Jenkins LTS 2.568.2 and earlier.
- Fixed versions: Jenkins weekly 2.580; Jenkins LTS 2.568.3.
- Risk: High. An authenticated user able to update a job configuration can overwrite object identity/ownership state, forge Stapler traversal to privileged controller functions, and execute attacker-selected commands as the Jenkins controller process. The same primitive also corrupts protected cross-item state: this proof made a separate
targetjob disappear from Jenkins' item map during an ordinary rename.
Impact Parity
- Disclosed/claimed maximum impact: Controller code execution through the remote Jenkins configuration API.
- Reproduced impact: Two fresh Jenkins 2.579 controller processes accepted crafted
config.xml, exposed the forged/job/carrier/pollingLog/run/project/parent/scriptTextroute, evaluated attacker-selected Groovy, and ran/bin/shto create unique controller-local marker files. Two Jenkins 2.580 controls accepted the same endpoint request but returned HTTP 404 on the forged route and created no marker. - Parity:
full - Not demonstrated: No privilege beyond the Jenkins controller process account or host/container escape was attempted; neither is required by the claim.
Root Cause
AbstractItem.updateByXml(Source) checks Item.CONFIGURE, then unmarshals the submitted XML directly into the existing item using Items.XSTREAM2.unmarshal(..., this, ..., true). Before the fix, RobustReflectionConverter.fieldDefinedInClass treated every reflected field as deserializable, deliberately including Java transient fields for old-configuration compatibility. There was no field-level mechanism to reject attacker values for runtime-only identity, parent, owner, or execution references.
The exploit composes those writes as follows:
- The persistent
Actionable.actionslist receiveshudson.triggers.SCMTrigger$BuildAction. - Its transient
runfield receives a freshly constructedhudson.model.FreeStyleBuild. Run.projectreceives a nestedFreeStyleProject.- The nested project's transient
AbstractItem.parentreceives a nestedjenkins.model.Jenkins. - That nested Jenkins object's ordinary
authorizationStrategyreceivesAuthorizationStrategy$Unsecured. - Stapler follows
carrier -> pollingLog -> run -> project -> parent -> scriptText.Jenkins.doScriptTextchecks the ACL of the nested Jenkins object, which now grants all permissions, rather than the real controller ACL. Attacker Groovy is evaluated on the real controller JVM and launches the selected command.
The same request sets transient AbstractItem.name to target. updateByXml calls onLoad afterward and restores the visible carrier name, but an ordinary rename records the injected value as oldName before restoration in the relevant flow and removes/replaces the protected item-map entry only on the vulnerable build. This supplies independent evidence of transient configuration-object overwrite.
The fix is commit 6244ebadf5b30257fde506c2d12d8eac9b6bc0e6 ([SECURITY-4032]). It adds @XStreamNotDeserializable / @XStreamDeserializable, teaches RobustReflectionConverter to skip forbidden transient fields, and annotates security-sensitive core fields including AbstractItem.name, AbstractItem.parent, Run.project, and SCMTrigger.BuildAction.run.
Reproduction Steps
- Run
bundle/repro/reproduction_steps.shfrom any directory. The script resolves its bundle throughPRUVA_ROOT, readsbundle/project_cache_context.json, and uses the prepared repository location when available. - The script pulls and verifies immutable Jenkins 2.579 and 2.580 JDK 21 image digests, downloads checksum-pinned Matrix Authorization Strategy dependencies, and creates four clean Jenkins controllers: two vulnerable and two fixed controls.
- Each controller is configured with an
attackeraccount possessingOverall/Read,Item/Read, andItem/Configure, but notOverall/Administer; direct/scriptaccess is captured as denied. - Through the real authenticated
/job/carrier/config.xmlendpoint, the script submits the crafted transient-field object graph. It then posts attacker-selected Groovy to the forged Stapler route and checks a unique controller-local command marker. - It performs the secondary poisoned-name rename check, finalizes per-attempt request/response/service evidence, and writes
bundle/repro/runtime_manifest.jsonwith direct SHA-256 bindings. - Expected result: both Jenkins 2.579 attempts show
config=200 route=200 marker=true, while both 2.580 attempts showconfig=200 route=404 marker=false. The script exits 0 only if all vulnerable and fixed assertions hold.
Evidence
bundle/logs/reproduction_steps.log: top-level diagnostic transcript.bundle/repro/runtime_manifest.json: immutable target identities and SHA-256 mapping for 48 finalized proof artifacts.bundle/repro/proof/vulnerable_1.result.txtandvulnerable_2.result.txt: HTTP 200 on the config and forged script route,marker_present=true, target removed.bundle/repro/proof/vulnerable_1.marker.txtandvulnerable_2.marker.txt: unique bytes written by attacker-selected/bin/shcommands.bundle/repro/proof/vulnerable_1.config.request.xml: exact transient-field object graph sent through Item/Configure.bundle/repro/proof/vulnerable_1.route.request.txtand.route.response.body: forged route and successful attacker Groovy output.bundle/repro/proof/fixed_1.result.txtandfixed_2.result.txt: HTTP 404 on the same route,marker_present=false, and preservedtarget-secretstate.bundle/repro/proof/fixed_1.marker.txtandfixed_2.marker.txt: explicit absence records for unique fixed-control markers.bundle/repro/proof/*_*.script.headers: direct Script Console controls showingX-Required-Permission: hudson.model.Hudson.Administerfor the low-privilege user (supporting diagnostic evidence; the finalized manifest binds the per-attempt exploit records).- Vulnerable image:
jenkins/jenkins@sha256:a7342867ea33efaacf825229d50b7fc77c144ecada9719ab4e32419f5d7412be, source release commit9095ea3a5c5e7dcd392695a5dd880af1c9910ddf. - Fixed image:
jenkins/jenkins@sha256:0e50a5b11ac14f3b84e529d725ed3a1c4b17ba16188dfa8d9a0189428b0839b1, source release commit497de4961ad80d97e26bfdeb0d2e40442a84ecb0.
Key latest-run transcript:
[vulnerable/1] config=200 route=200 marker=true rename=302 target=404: owned=200:carrier-updated-through-config-api
[vulnerable/2] config=200 route=200 marker=true rename=302 target=404: owned=200:carrier-updated-through-config-api
[fixed/1] config=200 route=404 marker=false rename=302 target=200:target-secret owned=200:carrier-updated-through-config-api
[fixed/2] config=200 route=404 marker=false rename=302 target=200:target-secret owned=200:carrier-updated-through-config-api
Recommendations / Next Steps
- Upgrade to Jenkins weekly 2.580 or newer, or LTS 2.568.3 or newer.
- Do not enable the
hudson.util.RobustReflectionConverter.DISABLE_XSTREAM_NOT_DESERIALIZABLE_CHECKescape hatch; it disables SECURITY-4032 protection. - Annotate plugin transient fields that encode identity, ownership, parentage, ACLs, runtime execution state, or object-root references with an annotation whose simple name is
XStreamNotDeserializable, as supported for plugins targeting older cores. - Use
@XStreamDeserializableonly for reviewed migration-only transient fields and add vulnerable/fixed tests through realconfig.xmlendpoints, not only direct XStream unit tests. - Audit plugin model types reachable from user-submitted configuration for unannotated transient
Item,Run,Job,Node,User, ACL, or controller references.
Additional Notes
- The final script passed two complete consecutive executions. Each execution itself launches two fresh vulnerable and two fresh fixed controller processes.
- The primary proof is the normal Jenkins HTTP product path; no sanitizer, direct parser harness, or mocked response is used.
- Rootless Docker emits a benign cgroup warning. The script uses a checksum-pinned Alpine cleanup image to remove subordinate-ID files left by interrupted runs and preserves caller-owned bundle paths.
- The Matrix Authorization Strategy plugin is used only to establish the stated low-privilege precondition. Its downloaded bytes and dependency are checksum pinned. The vulnerability and exploit object graph are in Jenkins core.
CVE-2026-84650 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-84650
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-84650
FAQ: CVE-2026-84650
Is CVE-2026-84650 exploitable?
How severe is CVE-2026-84650?
What type of vulnerability is CVE-2026-84650?
Which versions of jenkinsci/jenkins are affected by CVE-2026-84650?
How can I reproduce CVE-2026-84650?
Is the CVE-2026-84650 reproduction verified?
References for CVE-2026-84650
Authoritative sources for CVE-2026-84650 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.