# REPRO-2026-00349: Jenkins Stapler form binding instantiates arbitrary config types → RCE (SECURITY-3966) ## Summary Status: published Severity: high CVSS: Unknown CWE: CWE-502 (Deserialization of Untrusted Data) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00349 CVE: CVE-2026-84647 ## Package Name: jenkinsci/jenkins Ecosystem: github Affected: Unknown Fixed: Unknown ## Root Cause # 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` 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. ## Reproduction Details Reproduced: 2026-09-11T10:13:28.688Z Duration: 3063 seconds Tool calls: 422 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00349 pruva-verify CVE-2026-84647 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00349&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00349/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-84647 - Source: https://www.jenkins.io/security/advisory/2026-09-02/ ## Artifacts - bundle/repro/rca_report.md (analysis, 10592 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 15760 bytes) - bundle/logs/reproduction_steps.log (log, 828 bytes) - bundle/repro/proof/fixed_1.health.after-restart.txt (other, 47 bytes) - bundle/repro/proof/fixed_1.health.txt (other, 47 bytes) - bundle/repro/proof/fixed_1.identity.txt (other, 142 bytes) - bundle/repro/proof/fixed_1.marker.txt (other, 0 bytes) - bundle/repro/proof/fixed_1.privilege.txt (other, 69 bytes) - bundle/repro/proof/fixed_1.request.json (other, 581 bytes) - bundle/repro/proof/fixed_1.response.headers (other, 461 bytes) - bundle/repro/proof/fixed_1.result.txt (other, 96 bytes) - bundle/repro/proof/fixed_1.service.before.log (log, 4494 bytes) - bundle/repro/proof/fixed_1.service.log (log, 9476 bytes) - bundle/repro/proof/fixed_2.health.after-restart.txt (other, 47 bytes) - bundle/repro/proof/fixed_2.health.txt (other, 47 bytes) - bundle/repro/proof/fixed_2.identity.txt (other, 142 bytes) - bundle/repro/proof/fixed_2.marker.txt (other, 0 bytes) - bundle/repro/proof/fixed_2.negative_control.json (other, 278 bytes) - bundle/repro/proof/fixed_2.privilege.txt (other, 69 bytes) - bundle/repro/proof/fixed_2.request.json (other, 581 bytes) - bundle/repro/proof/fixed_2.response.headers (other, 461 bytes) - bundle/repro/proof/fixed_2.result.txt (other, 96 bytes) - bundle/repro/proof/fixed_2.service.before.log (log, 4494 bytes) - bundle/repro/proof/fixed_2.service.log (log, 9575 bytes) - bundle/repro/proof/vulnerable_1.health.after-restart.txt (other, 47 bytes) - bundle/repro/proof/vulnerable_1.health.txt (other, 47 bytes) - bundle/repro/proof/vulnerable_1.identity.txt (other, 142 bytes) - bundle/repro/proof/vulnerable_1.privilege.txt (other, 69 bytes) - bundle/repro/proof/vulnerable_1.result.txt (other, 94 bytes) - bundle/repro/proof/vulnerable_1.service.before.log (log, 4491 bytes) - bundle/repro/proof/vulnerable_2.health.after-restart.txt (other, 47 bytes) - bundle/repro/proof/vulnerable_2.health.txt (other, 47 bytes) - bundle/repro/proof/vulnerable_2.identity.txt (other, 142 bytes) - bundle/repro/proof/vulnerable_2.privilege.txt (other, 69 bytes) - bundle/repro/proof/vulnerable_2.response.headers (other, 466 bytes) - bundle/repro/proof/vulnerable_2.result.txt (other, 94 bytes) - bundle/repro/proof/vulnerable_2.service.before.log (log, 4492 bytes) - bundle/repro/runtime_manifest.json (other, 7673 bytes) - bundle/repro/validation_verdict.json (other, 1570 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00349 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00349/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00349 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev