# REPRO-2026-00350: Jenkins Stapler: CSRF crumb exposed in dynamically generated JavaScript endpoint (SECURITY-3607) ## Summary Status: published Severity: medium CVSS: Unknown CWE: CWE-352 (Cross-Site Request Forgery (CSRF)) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00350 CVE: CVE-2026-84649 ## Package Name: jenkinsci/jenkins Ecosystem: github Affected: Unknown Fixed: Unknown ## Root Cause # Root Cause Analysis: CVE-2026-84649 (SECURITY-3878) ## Summary Jenkins 2.579 bundles Stapler 2107.v8dfcb_e8ed317, whose dynamically generated JavaScript proxy endpoint serializes the authenticated user's CSRF crumb as a JavaScript string literal. A page controlled by an attacker on a sibling host in the same site can load that endpoint as a script, define `makeStaplerProxy`, capture the literal, and submit a cross-origin HTML form carrying the stolen crumb. In the current production-path proof, real Chromium logged in a victim, visited `attacker.example.test`, loaded the script from `jenkins.example.test`, recovered the crumb, and created a Jenkins API token through a POST that returned HTTP 403 without the crumb and HTTP 200 with the stolen crumb. Jenkins 2.580 replaces the literal with a DOM expression and is not vulnerable through this endpoint. ## Impact - **Affected component:** Jenkins core's Stapler integration and Stapler's dynamic JavaScript proxy endpoint `/$stapler/bound/script/...`. - **Affected versions:** Stapler 1839.ved17667b_a_eb_5 through 2107.v8dfcb_e8ed317 inclusive, except 2088.2093.vd7c3e58008a_6; Jenkins weekly 2.447 through 2.579 inclusive; Jenkins LTS 2.452.1 through 2.568.2 inclusive. - **Risk and consequences:** High according to Jenkins SECURITY-3878 (the ticket metadata said medium). The prerequisite is attacker-controlled active content on the same site as Jenkins, such as a sibling subdomain; the Resource Root URL being configured on that same site makes this particularly impactful. A victim must visit the attacker content while logged in. The attacker page can then authorize state-changing actions within the victim's Jenkins permissions. This run demonstrated creation of a new API token in the victim account. ## Impact Parity - **Disclosed/claimed maximum impact:** CSRF crumb theft leading to full authenticated-session action forgery (`authz_bypass`). - **Reproduced impact from this run:** A real sibling-host attacker page exfiltrated the victim's crumb in Chromium and used an ordinary cross-origin HTML form to create a victim API token. An identical state-changing POST without the stolen crumb returned HTTP 403; the forged POST returned HTTP 200 and produced token UUID `e81361f4-1998-41ca-98e0-dd36d6a7b747` in the final run. - **Parity:** `full`. - **Not demonstrated:** No stronger impact than actions already authorized to the victim was claimed or tested. The issued secret token value is intentionally not copied into this report. ## Root Cause Jenkins initializes Stapler's `org.kohsuke.stapler.CrumbIssuer` adapter in `core/src/main/java/hudson/security/csrf/CrumbIssuer.java`. In the vulnerable release, the adapter implements `issueCrumb` by returning `Jenkins.get().getCrumbIssuer().getCrumb(request)`. Stapler's `Bound.getProxyScript` consumes that value when rendering its dynamic proxy resource and single-quotes it in output such as: ```javascript varname = makeStaplerProxy('/whatever','SESSION_CRUMB',['methodname']); ``` Script inclusion is not protected by the browser same-origin read restriction in the way `fetch` is: a sibling-host page can load this JavaScript and provide its own `makeStaplerProxy` function to receive the literal argument. Because the sibling hosts are same-site, a victim's Jenkins session cookie is also eligible for the subsequent form POST. The endpoint therefore reveals the second anti-CSRF factor to the party that can already induce the browser to send the first factor. Fix commit [`95e24c6cfa75efd4a7c079ef091df6fd02748d1a`](https://github.com/jenkinsci/jenkins/commit/95e24c6cfa75efd4a7c079ef091df6fd02748d1a) (`[SECURITY-3878]`) changes the adapter to override `getCrumbExpression()` and return `document.head.dataset.crumbValue` when CSRF protection is enabled. Stapler 2117.vb_66952503166 then emits: ```javascript varname = makeStaplerProxy('/whatever',document.head.dataset.crumbValue,['methodname']); ``` The secret is no longer present in the cross-host script response. Jenkins 2.580 contains the fix; 2.579 does not. The tested peeled release commits were `9095ea3a5c5e7dcd392695a5dd880af1c9910ddf` and `497de4961ad80d97e26bfdeb0d2e40442a84ecb0` respectively. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` from any directory. The script reads `PRUVA_ROOT` or resolves the bundle path itself. 2. The script pulls and verifies these exact images: - vulnerable Jenkins 2.579 JDK 21: `sha256:a7342867ea33efaacf825229d50b7fc77c144ecada9719ab4e32419f5d7412be`; - fixed Jenkins 2.580 JDK 21: `sha256:0e50a5b11ac14f3b84e529d725ed3a1c4b17ba16188dfa8d9a0189428b0839b1`; - Playwright v1.55.0 Noble: `sha256:b27e719ecbfef153e13fd24e8341736733bf2658b229677eb21ff57ff5d7fb29`. 3. It starts real Jenkins controllers, creates and form-authenticates a victim, executes `bundle/repro/same_site_attack.js` in real Chromium, and serves an in-browser attacker page at `attacker.example.test` which loads the Jenkins script endpoint at sibling `jenkins.example.test`. 4. It submits the state-changing token-generation action with an actual cross-origin HTML form using the captured crumb. It also runs two isolated direct vulnerable attempts and two Jenkins 2.580 fixed negative-control attempts. 5. Expected success output is `CONFIRMED: Jenkins 2.579 exposed a session crumb ...; Jenkins 2.580 did not.` The script exits nonzero unless the no-crumb request is 403, the vulnerable stolen-crumb request is 200, and both fixed attempts omit the literal and emit the safe DOM expression. ## Evidence - **Real browser exploit result:** `bundle/repro/proof/browser-same-site-result.json` - `attacker_origin=http://attacker.example.test:8081/attack.html` - `victim_authenticated_as=victim` - `no_crumb_http=403` - `stolen_crumb_http=200` - `created_token_uuid=e81361f4-1998-41ca-98e0-dd36d6a7b747` - **Browser-created state-change marker:** `bundle/repro/proof/browser-same-site-marker.txt`. - **Browser/Jenkins service transcript:** `bundle/repro/proof/browser-same-site.service.log`. - **Two direct vulnerable attempt summaries:** `bundle/repro/proof/vulnerable_1.result.txt` and `vulnerable_2.result.txt`. Each records authentication, no-crumb HTTP 403, equality between the dynamic-script literal and the authenticated session crumb, and stolen-crumb HTTP 200. - **State-changing responses:** `bundle/repro/proof/vulnerable_1.stolen_crumb.response.body` and `vulnerable_2.stolen_crumb.response.body` contain Jenkins `status: ok` and fresh token UUIDs. - **Fixed negative controls:** `bundle/repro/proof/fixed_1.dynamic-js.response.body` and `fixed_2.dynamic-js.response.body` contain `document.head.dataset.crumbValue`, while both fixed result files record an empty dynamic literal and no attempted forged action. - **Loaded product/component identity:** `bundle/repro/proof/*product-component-identity.txt` binds the exact WAR SHA-256 and bundled Stapler jars (2107 vulnerable, 2117 fixed). - **Image identities:** `bundle/repro/proof/target-identities.txt`. - **Hash binding:** `bundle/repro/runtime_manifest.json` lists 48 finalized proof artifacts and the SHA-256 of each. It identifies the executed vulnerable image and peeled Jenkins commit. - **Root-cause source evidence:** `bundle/logs/repro/security-3878-source-diff.txt`, `vulnerable-source-snippet.txt`, `fixed-source-snippet.txt`, and `fix-commit-identity.txt`. - **Repeated execution:** `bundle/logs/reproduction_steps-redacted1.stdout` and `reproduction_steps-redacted2.stdout` both contain the confirmation line; both corresponding stderr logs are empty. No sanitizer was used. This was a production HTTP and browser workflow against the official Jenkins images, not a parser/unit harness. ## Recommendations / Next Steps - Upgrade Jenkins weekly to 2.580 or later, or LTS to 2.568.3 or later. - If immediate upgrade is impossible, disable the Resource Root URL feature or place it on a different registrable domain rather than a sibling host on the Jenkins site. - Do not host untrusted active content on the Jenkins site. - Preserve regression tests that request `/$stapler/bound/script/whatever?var=...&methods=...` with CSRF enabled and assert that the response contains no crumb-like literal and does contain the DOM expression. - Test with a real browser and sibling-host setup so script inclusion and SameSite cookie behavior remain covered. ## Additional Notes - The exact final script and helper passed twice consecutively after digest pinning and the cross-origin form enhancement. Their SHA-256 values are `487358e21524f0215abf5a00acf9898d07f00b04c995a7fb377285f0bc6f3914` and `88bf6d58888e4ec722c4739c968d8347c730f97ce8d40d21ba0169c1de2f0605`. - Every controller uses a fresh temporary Docker volume and every invocation removes its containers, network, and labeled volumes. - The ticket summary calls this `SECURITY-3607`; the Jenkins advisory, source test, and fix commit consistently identify it as `SECURITY-3878`. - The browser helper installs the exact `playwright@1.55.0` Node package into a temporary volume while reusing the matching immutable Playwright image's Chromium. Network access is therefore required for first-time image and package acquisition. ## Reproduction Details Reproduced: 2026-09-11T10:13:35.085Z Duration: 3116 seconds Tool calls: 472 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00350 pruva-verify CVE-2026-84649 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00350&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00350/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-84649 - Source: https://www.jenkins.io/security/advisory/2026-09-02/ ## Artifacts - bundle/repro/rca_report.md (analysis, 9217 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 17021 bytes) - bundle/logs/repro/security-3878-source-diff.txt (other, 2190 bytes) - bundle/logs/reproduction_steps-form1.stdout (other, 140 bytes) - bundle/logs/reproduction_steps-form2.stdout (other, 140 bytes) - bundle/logs/reproduction_steps-redacted1.stdout (other, 140 bytes) - bundle/logs/reproduction_steps-redacted2.stdout (other, 140 bytes) - bundle/repro/proof/browser-same-site-marker.txt (other, 37 bytes) - bundle/repro/proof/browser-same-site-result.json (other, 436 bytes) - bundle/repro/proof/browser-same-site-summary.txt (other, 189 bytes) - bundle/repro/proof/browser-same-site.log (log, 0 bytes) - bundle/repro/proof/browser-same-site.service.log (log, 3675 bytes) - bundle/repro/proof/fixed_1.dynamic-js.request.txt (other, 138 bytes) - bundle/repro/proof/fixed_1.dynamic-js.response.headers (other, 884 bytes) - bundle/repro/proof/fixed_1.no_crumb.request.txt (other, 235 bytes) - bundle/repro/proof/fixed_1.no_crumb.response.body (other, 603 bytes) - bundle/repro/proof/fixed_1.no_crumb.response.headers (other, 1017 bytes) - bundle/repro/proof/fixed_1.product-component-identity.txt (other, 305 bytes) - bundle/repro/proof/fixed_1.service.log (log, 3385 bytes) - bundle/repro/proof/fixed_2.dynamic-js.request.txt (other, 138 bytes) - bundle/repro/proof/fixed_2.dynamic-js.response.body (other, 88 bytes) - bundle/repro/proof/fixed_2.dynamic-js.response.headers (other, 884 bytes) - bundle/repro/proof/fixed_2.no_crumb.request.txt (other, 235 bytes) - bundle/repro/proof/fixed_2.no_crumb.response.body (other, 603 bytes) - bundle/repro/proof/fixed_2.no_crumb.response.headers (other, 1017 bytes) - bundle/repro/proof/fixed_2.product-component-identity.txt (other, 305 bytes) - bundle/repro/proof/fixed_2.result.txt (other, 267 bytes) - bundle/repro/proof/fixed_2.service.log (log, 3385 bytes) - bundle/repro/proof/target-identities.txt (other, 358 bytes) - bundle/repro/proof/vulnerable_1.dynamic-js.request.txt (other, 138 bytes) - bundle/repro/proof/vulnerable_1.dynamic-js.response.headers (other, 885 bytes) - bundle/repro/proof/vulnerable_1.no_crumb.request.txt (other, 235 bytes) - bundle/repro/proof/vulnerable_1.no_crumb.response.body (other, 603 bytes) - bundle/repro/proof/vulnerable_1.no_crumb.response.headers (other, 1017 bytes) - bundle/repro/proof/vulnerable_1.product-component-identity.txt (other, 305 bytes) - bundle/repro/proof/vulnerable_1.service.log (log, 3383 bytes) - bundle/repro/proof/vulnerable_1.stolen_crumb.request.txt (other, 315 bytes) - bundle/repro/proof/vulnerable_1.stolen_crumb.response.body (other, 172 bytes) - bundle/repro/proof/vulnerable_1.stolen_crumb.response.headers (other, 985 bytes) - bundle/repro/proof/vulnerable_2.dynamic-js.request.txt (other, 138 bytes) - bundle/repro/proof/vulnerable_2.dynamic-js.response.body (other, 122 bytes) - bundle/repro/proof/vulnerable_2.dynamic-js.response.headers (other, 885 bytes) - bundle/repro/proof/vulnerable_2.no_crumb.request.txt (other, 235 bytes) - bundle/repro/proof/vulnerable_2.no_crumb.response.body (other, 603 bytes) - bundle/repro/proof/vulnerable_2.no_crumb.response.headers (other, 1017 bytes) - bundle/repro/proof/vulnerable_2.product-component-identity.txt (other, 305 bytes) - bundle/repro/proof/vulnerable_2.result.txt (other, 336 bytes) - bundle/repro/proof/vulnerable_2.service.log (log, 3383 bytes) - bundle/repro/proof/vulnerable_2.stolen_crumb.request.txt (other, 315 bytes) - bundle/repro/proof/vulnerable_2.stolen_crumb.response.body (other, 172 bytes) - bundle/repro/proof/vulnerable_2.stolen_crumb.response.headers (other, 985 bytes) - bundle/repro/runtime_manifest.json (other, 9344 bytes) - bundle/repro/same_site_attack.js (other, 3961 bytes) - bundle/repro/validation_verdict.json (other, 1424 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00350 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00350/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00350 ## 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