Skip to content

CVE-2026-84648: Verified Reproduction

CVE-2026-84648: Jenkins stored XSS in system log viewer via agent log output SECURITY-3476

CVE-2026-84648 is verified against jenkinsci/jenkins · github. Affected versions: Jenkins weekly <= 2.579; Jenkins LTS <= 2.568.2. Vulnerability class: XSS. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00367.

REPRO-2026-00367 jenkinsci/jenkins · github XSS Sep 24, 2026 CVE entry .txt
Severity
HIGH
Confidence
HIGH
Reproduced in
68m 48s
Tool calls
272
Spend
$8.20
01 · Overview

What Is CVE-2026-84648?

CVE-2026-84648 is a high-severity XSS vulnerability affecting jenkinsci/jenkins Jenkins weekly <= 2.579; Jenkins LTS <= 2.568.2. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00367).

02 · Severity & CVSS

CVE-2026-84648 Severity

CVE-2026-84648 is rated high severity.

HIGH threat level
Weakness CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

Affected jenkinsci/jenkins Versions

jenkinsci/jenkins · github versions Jenkins weekly <= 2.579; Jenkins LTS <= 2.568.2 are affected.

How to Reproduce CVE-2026-84648

$ pruva-verify REPRO-2026-00367
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00367/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-84648

Cross-site scripting — reproduced
  • 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
Trigger

java.util.logging.LogRecord fields (sourceClassName) published by an attacker-controlled inbound agent process

Attack chain
  1. agent JNLP4 connect
  2. agent-side hudson.slaves.SlaveComputer ring buffer
  3. admin GET /log/agentlog/
  4. Functions.printLogRecordHtml renders metadata unescaped
  5. stored XSS executes in admin browser
How the agent worked 600 events · 272 tool calls · 1h 9m
1h 9mDuration
272Tool calls
113Reasoning steps
600Events
19Dead-ends
Agent activity over 1h 9m
Policy
1
Support
9
Repro
378
Judge
44
Variant
163
Verify
1
0:0068:35

Root Cause and Exploit Chain for CVE-2026-84648

Versions: Jenkins ≤ 2.579, LTS ≤ 2.568.2.

Jenkins 2.579 and earlier (LTS 2.568.2 and earlier) renders log record metadata — source, level, and timestamp — in the system log viewer without HTML escaping. An attacker who controls an agent process can publish a java.util.logging.LogRecord with an attacker-controlled sourceClassName (e.g. <svg/onload=…>). That record is captured in the agent-side ring buffer (SlaveComputer.LogHolder.SLAVE_LOG_HANDLER, attached agent-side to the hudson.slaves.SlaveComputer logger by SlaveInitializer during channel setup), fetched over the remoting channel when an administrator opens a log recorder page (/log/<name>/), and rendered raw into the HTML. This is a stored XSS in the administrator's session context. Jenkins 2.580 / LTS 2.568.3 fixes it by escaping the metadata with Util.xmlEscape.

  • Package/component: Jenkins core (hudson.Functions#printLogRecordHtml, rendered by lib/hudson/logRecords.jelly on hudson.logging.LogRecorder pages).
  • Affected versions: Jenkins ≤ 2.579, LTS ≤ 2.568.2.
  • Risk: High (CVSS 8.8, AV:N/AC:L/PR:N/UI:R). Stored XSS in the system log viewer executes in an administrator's authenticated browser session, enabling session hijack and full controller compromise (e.g. crumb theft → /scriptText Groovy RCE chain legs CVE-2026-84649 / CVE-2026-84645).
  • Attacker precondition: control of an agent process (malicious/compromised agent). The victim must open a log recorder page that targets the agent log namespace.

Impact Parity

  • Disclosed/claimed maximum impact: stored XSS executing in an administrator's session (session hijack; stepping stone to RCE).
  • Reproduced impact from this run: (see Evidence — filled from the runtime proof) unescaped attacker payload rendered on the real /log/agentlog/ page of Jenkins 2.579 served to an authenticated admin; headless-Chromium admin session executed the injected script, which exfiltrated the authenticated same-origin /whoAmI/api/json response to an attacker-controlled beacon. Jenkins 2.580 negative control renders the payload escaped and no beacon callback occurs.
  • Parity: full.
  • Not demonstrated: the downstream RCE chain legs (CVE-2026-84649 crumb theft, CVE-2026-84645 deserialization RCE) are separate tickets and out of scope here.

Root Cause

core/src/main/java/hudson/Functions.java (Jenkins 2.579), printLogRecordHtml(LogRecord r, LogRecord prior):

String[] oldParts = prior == null ? new String[4] : logRecordPreformat(prior);
String[] newParts = logRecordPreformat(r);
for (int i = 0; i < /* not 4 */3; i++) {
    newParts[i] = "<span class='" + (newParts[i].equals(oldParts[i]) ? "logrecord-metadata-old" : "logrecord-metadata-new") + "'>" + newParts[i] + "</span>";
}
newParts[3] = Util.xmlEscape(newParts[3]);

Only parts[3] (the message) is escaped. parts[0] (timestamp), parts[1] (source = sourceClassName [+ sourceMethodName], or loggerName when sourceClassName == null), and parts[2] (level) are concatenated into raw HTML. lib/hudson/logRecords.jelly then emits them with <j:out value="${parts[0..2]}"/>, which outputs raw (unescaped) HTML. A LogRecord whose sourceClassName is already set is not overwritten by Logger.log(LogRecord), so an attacker JVM fully controls this field.

Delivery path from the agent: SlaveComputer.SlaveInitializer (a MasterToSlaveCallable sent during setChannel) installs LogHolder.SLAVE_LOG_HANDLER (a RingBufferLogHandler) on the hudson.slaves.SlaveComputer logger inside the agent JVM. When an administrator views a LogRecorder page whose targets include that namespace, LogRecorder.getSlaveLogRecords() calls SlaveComputer.getLogRecords() → SlaveLogFetcher callable over the remoting channel → returns the agent ring buffer → records are rendered via the vulnerable function.

Fix (Jenkins 2.580): the same loop becomes

String cls = newParts[i].equals(oldParts[i]) ? "logrecord-metadata-old" : "logrecord-metadata-new";
newParts[i] = "<span class='" + cls + "'>" + Util.xmlEscape(newParts[i]) + "</span>";

Verified via git diff jenkins-2.579..jenkins-2.580 -- core/src/main/java/hudson/Functions.java and the added regression test test/src/test/java/hudson/logging/LogRecorderManagerTest.java#logRecorderPageDoesNotRenderUnescapedMetadata (@Issue("SECURITY-3967")).

Reproduction Steps

  1. bundle/repro/reproduction_steps.sh (self-contained; requires Docker, Python 3, Node.js, curl, jq).
  2. Per attempt (2 vulnerable on jenkins/jenkins:2.579, 2 fixed on jenkins/jenkins:2.580, fresh container each):
    • Starts Jenkins with an init groovy script that creates the admin account, an inbound (JNLP) agent node agent1, and a system log recorder agentlog targeting hudson.slaves.SlaveComputer at Level.ALL.
    • Downloads the real agent.jar from the running controller, compiles bundle/repro/agent/AgentXss.java against it inside the container, and connects an attacker-controlled agent process over the real JNLP4/remoting TCP boundary.
    • The agent publishes a LogRecord with sourceClassName = <svg/onload="fetch('/whoAmI/api/json').then(…exfiltrate to beacon…)"> under the hudson.slaves.SlaveComputer logger.
    • The administrator views http://127.0.0.1:18080/log/agentlog/ (curl capture of the raw HTML + response headers, and a headless-Chromium admin login + page visit).
  3. Expected evidence:
    • Vulnerable: raw <svg/onload=…> payload present verbatim in the served HTML; headless admin browser executes it and the attacker beacon receives the marker plus the exfiltrated authenticated /whoAmI/api/json body.
    • Fixed: HTML contains only &lt;svg/onload…; beacon never receives the marker.

Evidence

  • bundle/logs/reproduction_steps.log — full run log.
  • bundle/repro/proof/vulnerable_{1,2}/page.html — raw payload in served page (vulnerable).
  • bundle/repro/proof/vulnerable_1/beacon-hit.txt, bundle/repro/proof/beacon.log — attacker beacon callbacks proving script execution in the admin session.
  • bundle/repro/proof/vulnerable_{1,2}/agent.log — attacker agent channel + MALICIOUS_RECORD_PUBLISHED.
  • bundle/repro/proof/fixed_{1,2}/page.html — escaped payload only (negative control).
  • bundle/repro/proof/*/result.json — per-attempt structured results; bundle/repro/runtime_manifest.json — runtime evidence manifest with artifact hashes.
  • Environment: jenkins/jenkins:2.579 (sha256:a7342867ea33efaacf825229d50b7fc77c144ecada9719ab4e32419f5d7412be), jenkins/jenkins:2.580, bundled JDK 21, Linux x86_64.

Recommendations / Next Steps

  • Upgrade to Jenkins 2.580 / LTS 2.568.3 (metadata is escaped before rendering).
  • Enforcing the Jenkins Content Security Policy (opt-in before the fix; default is report-only) mitigates script execution but does not fix the missing escaping.
  • Treat agent hosts as within the threat boundary: any agent can inject HTML into controller UI pages viewed by admins.

Additional Notes

  • The script is idempotent: every attempt uses a fresh container (no persistent JENKINS_HOME), and reruns rebuild all proof artifacts.
  • Limitation: the browser execution proof uses headless Chromium via puppeteer; if Chromium cannot be installed in the replay sandbox, the script degrades to the HTML oracle only (raw vs escaped payload), which still directly demonstrates the missing escaping that the 2.580 fix addresses.
  • The source metadata line is only rendered when consecutive records differ; the agent publishes a benign control record before the malicious one so the payload-bearing span is always rendered.

CVE-2026-84648 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.

Event 1/40
0:003:08
0:00
session startedaccounts/fireworks/models/kimi-k3 · CVE-2026-84648 · REPRO-20
0:06
0:08
web search
0:09
0:47
0:47
extract_facts
no facts extracted
0:51
0:51
supportclaim_contract
1:05
1:05
1:05
1:05
1:09
1:09
1:12
1:12
1:18
1:18
1:33
1:34
$docker ps >/dev/null 2>&1 && echo DOCKER_OK || echo DOCKER_UNAVAILABLE; java -version 2>&1 | head -2; curl -sI https://registry-1.docker.io/v2/ -o /dev/null -w '%{http_code}\n' --max-time 10
1.0s✓
DOCKER_OK
pruva-command: 1: java: not found
401
1:36
web search
2:09
2:18
$which gh; docker images | grep -i jenkins; docker pull jenkins/jenkins:2.579 2>&1 | tail -3
9.0s✓
Digest: sha256:a7342867ea33efaacf825229d50b7fc77c144ecada9719ab4e32419f5d7412be
Status: Downloaded newer image for jenkins/jenkins:2.579
docker.io/jenkins/jenkins:2.579
2:23
$cd /pruva/project-cache && git clone --bare --filter=blob:none --no-checkout https://github.com/jenkinsci/jenkins.git repo-mirrors/jenkins.git 2>&1 | tail -3
4.8s✓
Cloning into bare repository 'repo-mirrors/jenkins.git'...
2:32
2:33
$cd /pruva/project-cache/repo-mirrors/jenkins.git && git diff --stat jenkins-2.579..jenkins-2.580 -- '*logging*' '*LogRecord*' 2>&1 | head -30; echo ===; git diff jenkins-2.579..jenkins-2.580 --name-only 2>/dev/null | grep -iE 'log' | head -30
0.8s✓
 .../hudson/logging/LogRecorderManagerTest.java     | 25 ++++++++++++++++++++++
 1 file changed, 25 insertions(+)
===
test/src/test/java/hudson/logging/LogRecorderManagerTest.java
2:37
$cd /pruva/project-cache/repo-mirrors/jenkins.git && git diff jenkins-2.579..jenkins-2.580 -- test/src/test/java/hudson/logging/LogRecorderManagerTest.java | head -80
0.4s✓
{"cwd":"/workspace","duration_ms":47,"exit_code":0,"stderr":"","stdout":"diff --git a/test/src/test/java/hudson/logging/LogRecorderManagerTest.java b/test/src/test/java/hudson/logging/LogRecorderManagerTest.java\nindex 799d62cc77..08b06cacac 100644\n--- a/test/src/test/java/hudson/logging/LogRecorderManagerTest.java\n+++ b/test/src/test/java/hudson/logging/LogRecorderManagerTest.java\n@@ -31,6 +31,7 @@ import static org.hamcrest.Matchers.containsString;\n import static org.hamcrest.Matchers.empty;\n import static org.hamcrest.Matchers.equalTo;\n import static org.hamcrest.Matchers.hasSize;\n+import static org.hamcrest.Matchers.not;\n import static org.junit.jupiter.api.Assertions.assertEquals;\n import static org.junit.jupiter.api.Assertions.assertFalse;\n import static org.junit.jupiter.api.Assertions.assertNotNull;\n@@ -203,6 +204,30 @@ class LogRecorderManagerTest {\n         assertFalse(text.contains(\"LambdaLog @FINER\"), text);\n     }\n \n+    @Issue(\"SECURITY-3967\")\n+    @Test\n+    void logRecorderPageDoesNotRenderUnescapedMetadata() throws Exception {\n+        LogRecorderManager mgr = j.jenkins.getLog();\n+        LogRecorder recorder = new LogRecorder(\"xsstest\");\n… [truncated]
3:08
08 · How to Fix

How to Fix CVE-2026-84648

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-84648 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-84648

Is CVE-2026-84648 exploitable?

Yes. Pruva independently reproduced CVE-2026-84648 in jenkinsci/jenkins and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00367).

How severe is CVE-2026-84648?

CVE-2026-84648 is rated high severity.

What type of vulnerability is CVE-2026-84648?

CVE-2026-84648 is classified as CWE-79 (Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')), a XSS vulnerability.

Which versions of jenkinsci/jenkins are affected by CVE-2026-84648?

jenkinsci/jenkins Jenkins weekly <= 2.579; Jenkins LTS <= 2.568.2 is affected by CVE-2026-84648.

How can I reproduce CVE-2026-84648?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-84648 reproduction verified?

Yes. Pruva reproduced CVE-2026-84648 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-84648

Authoritative sources for CVE-2026-84648 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.