# REPRO-2026-00367: Jenkins stored XSS in system log viewer via agent log output (SECURITY-3476) ## Summary Status: published Severity: high CVSS: Unknown CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00367 CVE: CVE-2026-84648 ## Package Name: jenkinsci/jenkins Ecosystem: github Affected: Jenkins weekly <= 2.579; Jenkins LTS <= 2.568.2 Fixed: Unknown ## Root Cause # RCA Report — CVE-2026-84648 (SECURITY-3967): Jenkins stored XSS in system log viewer via agent log output ## Summary 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. ``). 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//`), 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`. ## Impact - 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)`: ```java String[] oldParts = prior == null ? new String[4] : logRecordPreformat(prior); String[] newParts = logRecordPreformat(r); for (int i = 0; i < /* not 4 */3; i++) { newParts[i] = "" + newParts[i] + ""; } 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 ``, 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 ```java String cls = newParts[i].equals(oldParts[i]) ? "logrecord-metadata-old" : "logrecord-metadata-new"; newParts[i] = "" + Util.xmlEscape(newParts[i]) + ""; ``` 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 = ` 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 `` 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 `<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. ## Reproduction Details Reproduced: 2026-09-24T17:06:29.504Z Duration: 4128 seconds Tool calls: 272 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00367 pruva-verify CVE-2026-84648 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00367&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00367/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-84648 - Source: https://www.jenkins.io/security/advisory/2026-09-02/ ## Artifacts - bundle/repro/rca_report.md (analysis, 7753 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 15929 bytes) - bundle/repro/agent/AgentXss.java (other, 4003 bytes) - bundle/repro/beacon.py (script, 1024 bytes) - bundle/repro/browser/visit.js (other, 2422 bytes) - bundle/repro/init.groovy.d/01-setup.groovy (other, 1924 bytes) - bundle/repro/proof/beacon.log (log, 565 bytes) - bundle/repro/proof/fixed_1/agent.log (log, 1577 bytes) - bundle/repro/proof/fixed_1/browser.log (log, 336 bytes) - bundle/repro/proof/fixed_1/page.headers (other, 1652 bytes) - bundle/repro/proof/fixed_1/page.html (other, 186460 bytes) - bundle/repro/proof/fixed_1/result.json (other, 196 bytes) - bundle/repro/proof/fixed_2/agent.log (log, 1575 bytes) - bundle/repro/proof/fixed_2/page.headers (other, 1652 bytes) - bundle/repro/proof/fixed_2/page.html (other, 185881 bytes) - bundle/repro/proof/fixed_2/result.json (other, 195 bytes) - bundle/repro/proof/vulnerable_1/agent.log (log, 1587 bytes) - bundle/repro/proof/vulnerable_1/beacon-hit.txt (other, 283 bytes) - bundle/repro/proof/vulnerable_1/browser.log (log, 1347 bytes) - bundle/repro/proof/vulnerable_1/page.headers (other, 1651 bytes) - bundle/repro/proof/vulnerable_1/page.html (other, 186464 bytes) - bundle/repro/proof/vulnerable_1/payload.txt (other, 276 bytes) - bundle/repro/proof/vulnerable_1/result.json (other, 205 bytes) - bundle/repro/proof/vulnerable_2/agent.log (log, 1585 bytes) - bundle/repro/proof/vulnerable_2/page.headers (other, 1651 bytes) - bundle/repro/proof/vulnerable_2/page.html (other, 186511 bytes) - bundle/repro/proof/vulnerable_2/payload.txt (other, 274 bytes) - bundle/repro/proof/vulnerable_2/result.json (other, 204 bytes) - bundle/repro/runtime_manifest.json (other, 4437 bytes) - bundle/repro/validation_verdict.json (other, 1296 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00367 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00367/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00367 ## 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