CVE-2026-84649: Verified Reproduction
CVE-2026-84649: Jenkins Stapler: CSRF crumb exposed in dynamically generated JavaScript endpoint SECURITY-3607
CVE-2026-84649 is verified against jenkinsci/jenkins · github. Vulnerability class: CSRF. This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00350.
What Is CVE-2026-84649?
CVE-2026-84649 is a medium-severity CSRF vulnerability affecting jenkinsci/jenkins. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00350).
CVE-2026-84649 Severity
CVE-2026-84649 is rated medium severity.
Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.
How to Reproduce CVE-2026-84649
pruva-verify REPRO-2026-00350 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00350/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-84649
- 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
A same-site sibling-host HTML page that includes the Jenkins /$stapler/bound/script endpoint, captures its crumb argument, and submits it in a cross-origin form
- Chromium victim login
- attacker.example.test page
- GET /$stapler/bound/script/whatever
- forged POST /me/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken
How the agent worked
Root Cause and Exploit Chain for CVE-2026-84649
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.
- 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-dd36d6a7b747in 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:
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 ([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:
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
- Run
bundle/repro/reproduction_steps.shfrom any directory. The script readsPRUVA_ROOTor resolves the bundle path itself. - 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.
- vulnerable Jenkins 2.579 JDK 21:
- It starts real Jenkins controllers, creates and form-authenticates a victim, executes
bundle/repro/same_site_attack.jsin real Chromium, and serves an in-browser attacker page atattacker.example.testwhich loads the Jenkins script endpoint at siblingjenkins.example.test. - 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.
- 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.jsonattacker_origin=http://attacker.example.test:8081/attack.htmlvictim_authenticated_as=victimno_crumb_http=403stolen_crumb_http=200created_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.txtandvulnerable_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.bodyandvulnerable_2.stolen_crumb.response.bodycontain Jenkinsstatus: okand fresh token UUIDs. - Fixed negative controls:
bundle/repro/proof/fixed_1.dynamic-js.response.bodyandfixed_2.dynamic-js.response.bodycontaindocument.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.txtbinds 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.jsonlists 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, andfix-commit-identity.txt. - Repeated execution:
bundle/logs/reproduction_steps-redacted1.stdoutandreproduction_steps-redacted2.stdoutboth 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
487358e21524f0215abf5a00acf9898d07f00b04c995a7fb377285f0bc6f3914and88bf6d58888e4ec722c4739c968d8347c730f97ce8d40d21ba0169c1de2f0605. - 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 asSECURITY-3878. - The browser helper installs the exact
playwright@1.55.0Node 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.
CVE-2026-84649 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-84649
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-84649
FAQ: CVE-2026-84649
Is CVE-2026-84649 exploitable?
How severe is CVE-2026-84649?
What type of vulnerability is CVE-2026-84649?
How can I reproduce CVE-2026-84649?
Is the CVE-2026-84649 reproduction verified?
References for CVE-2026-84649
Authoritative sources for CVE-2026-84649 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.