Yeonoh Park

Technical write-up · Jenkins

Stored XSS in update-center2 download pages

How missing output encoding allowed plugin metadata to become active content in Jenkins update-center2 download pages, and the defensive lesson from CVE-2026-84677.

Publishing a plugin and controlling a website

A plugin maintainer could supply display metadata for their own release that became active browser content on a generated Jenkins download page. update-center2 builds those pages from plugin information. The maintainer is allowed to publish descriptive text for that plugin, but should not thereby control executable content on a Jenkins-operated page. A visitor encounters the page before choosing to download or install the plugin. Jenkins published this stored XSS issue as CVE-2026-84677.

The research question was whether an ordinary plugin-specific publishing permission could cross into control of the centrally generated website. My original local validation used the official update-center2 3.18.3 generator and browser. The 3.18.4 correction discussed below comes from a later review of published release source, not a new runtime test.

Where publishing met rendering

Jenkins’s publishing guidance requires permission to release the particular plugin. That was the starting capability I examined: authority over one plugin’s metadata, not over update-center2, the update-site signing key, or Jenkins web infrastructure. In my original local check, ordinary release validation and packaging accepted the controlled display value. This was important: if publishing had rejected the value, the claimed boundary crossing would not have followed from a legitimate maintainer’s authority. No test content was uploaded to Jenkins.

Accepting a value for publication is distinct from displaying it safely. In the published 3.18.3 IndexHtmlBuilder.close source, a plugin-provided title was substituted directly into generated HTML:

.replace("{{ title }}", title)

The important distinction is not whether the maintainer could provide the metadata; that is an intended part of publishing a plugin. The failure was allowing that data to take on a browser-active meaning when the centrally generated page displayed it.

What the original controls established

Source review alone would establish a risky rendering decision, not the browser result. My original local browser assessment of update-center2 3.18.3 compared generated download pages using controlled display values. The checks recorded a harmless browser-side marker only when the value was rendered without encoding. The following summarizes the preserved outcomes, not a reproduction recipe or a new run performed for this article.

Local comparison Browser observation What it distinguishes
Publisher-controlled display value rendered without encoding The harmless marker appeared. The generated page produced an active browser effect, not just unusual source text.
Ordinary plain-text value No marker appeared. Page generation or browser navigation alone did not cause the effect.
Display value encoded for HTML No marker appeared. Treating metadata as text removed the observed effect.

The local publishing check answers a different alternative explanation: that normal release handling would reject the metadata before page generation. It accepted the input in a disposable environment, but it did not prove that malicious content was ever published on Jenkins infrastructure. The Jenkins advisory describes the wider problem across plugin names, descriptions, and version metadata; my original browser observation specifically concerned a display name.

Impact and its limits

The demonstrated effect was script execution in a locally generated download page, which crosses the website-content boundary without installing a plugin. Such execution can change what a visitor sees on that page. I did not test a malicious public release, real visitor navigation, artifact installation, credential access, or a Jenkins controller. In particular, browser execution on a download page is not evidence of controller-origin script execution or administrator compromise.

Jenkins rates the issue Medium 5.4 under CVSS 3.1 and identifies 3.18.3 and earlier as affected. The original runtime evidence concerns 3.18.3; the wider range is the vendor’s assessment, not a claim that I tested every earlier release. I do not infer a first affected version from the snapshots reviewed here.

Reading the released correction

Jenkins says update-center2 3.18.4 escapes plugin names, descriptions, and version metadata. In the released 3.18.4 IndexHtmlBuilder.close source, the same rendering point now encodes the title before substitution:

.replace("{{ title }}", escapeXml(title))

The same released source also escapes descriptions, version captions, and other generated fields. This changes the decision where metadata becomes HTML: the publisher still supplies the value, but the generator presents it as data. I compared the published 3.18.3 and 3.18.4 release source for this article. That supports the mechanism of the shipped correction; it is not a patched-release browser result. Independent runtime verification of 3.18.4 has not been repeated.

Useful regression cases would exercise the actual generated pages, rather than only an escaping helper. These are proposed tests, not executed results or a claim about upstream test coverage.

Proposed case Required behavior
Ordinary plugin name and description Remain readable in the title, heading, and description.
Markup-like name or description Remain inert text wherever the template displays them.
Unusual version metadata Remain text in the generated version list and attributes.

The remaining validation step is to run those controls against the released 3.18.4 generator and confirm both correct display and the absence of active browser content. The original local observation and the later source comparison answer different questions; neither should be stretched into an untested public-site or controller-impact claim.