CVE-2025-47885
Description
Jenkins Health Advisor by CloudBees Plugin 374.v194b_d4f0c8c8 and earlier does not escape responses from the Jenkins Health Advisor server, resulting in a stored cross-site scripting (XSS) vulnerability exploitable by attackers able to control Jenkins Health Advisor server responses.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Health Advisor by CloudBees Plugin fails to escape server responses, enabling stored XSS for attackers controlling the advisor server.
The Jenkins Health Advisor by CloudBees Plugin, versions 374.v194b_d4f0c8c8 and earlier, does not escape responses received from the Jenkins Health Advisor server. This missing output sanitization allows arbitrary HTML and JavaScript to be injected into the Jenkins web interface when the plugin displays server responses, such as error messages or bundle upload results [1][2].
To exploit this stored cross-site scripting (XSS) vulnerability, an attacker must be able to control the responses sent by the Jenkins Health Advisor server. This could be achieved through a man-in-the-middle attack on the communication between the Jenkins controller and the advisor server, or by compromising the advisor server itself. No additional authentication is required beyond the ability to influence the server's responses [2].
Successful exploitation allows the attacker to execute arbitrary JavaScript in the context of the Jenkins web UI. This can lead to session hijacking, credential theft, or other malicious actions performed with the privileges of the victim user viewing the affected page [1][2].
The vulnerability has been addressed in version 374.376.v3a_41a_a_142efe of the plugin, which properly escapes server responses using Util.xmlEscape() before rendering them in the Jenkins interface [3]. Users are strongly advised to update to this or a later version to mitigate the risk.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.plugins:cloudbees-jenkins-advisorMaven | < 374.376.v3a_41a_a_142efe | 374.376.v3a_41a_a_142efe |
Affected products
2- Range: <=374.v194b_d4f0c8c8
- Jenkins Project/Jenkins Health Advisor by CloudBees Pluginv5Range: 0
Patches
14b456b3110d1SECURITY-3559
2 files changed · +25 −3
src/main/java/com/cloudbees/jenkins/plugins/advisor/BundleUpload.java+7 −3 modified@@ -7,6 +7,7 @@ import com.cloudbees.jenkins.plugins.advisor.client.model.Recipient; import com.cloudbees.jenkins.support.SupportPlugin; import hudson.Extension; +import hudson.Util; import hudson.model.AsyncPeriodicWork; import hudson.model.TaskListener; import hudson.security.ACL; @@ -102,11 +103,12 @@ private File generateBundle() { } } catch (Exception e) { logError(COULD_NOT_SAVE_SUPPORT_BUNDLE, e); + var sanitizedMessage = Util.xmlEscape(e.getMessage()); updateLastBundleResult( config, createTimestampedErrorMessage( "<strong>%s</strong><br/><pre><code>%s</code></pre>", - COULD_NOT_SAVE_SUPPORT_BUNDLE, e.getMessage())); + COULD_NOT_SAVE_SUPPORT_BUNDLE, sanitizedMessage)); if (file != null && file.exists() && !file.delete()) { log(Level.WARNING, "Could not delete bundle {0}" + file); } @@ -124,22 +126,24 @@ private void executeInternal(String email, File file, String pluginVersion) { if (response.getCode() == 200) { updateLastBundleResult(config, createTimestampedInfoMessage(BUNDLE_SUCCESSFULLY_UPLOADED)); } else { + var sanitizedMessage = Util.xmlEscape(response.getMessage()); updateLastBundleResult( config, createTimestampedErrorMessage( "<strong>Bundle upload failed</strong><br/>Server response is: <code>%d - %s</code>", - response.getCode(), response.getMessage())); + response.getCode(), sanitizedMessage)); } } catch (Exception e) { log(Level.SEVERE, "Issue while uploading file to bundle upload service: " + e.getMessage()); log( Level.FINEST, "Exception while uploading file to bundle upload service. Cause: " + ExceptionUtils.getStackTrace(e)); + var sanitizedMessage = Util.xmlEscape(e.getMessage()); updateLastBundleResult( config, createTimestampedErrorMessage( - "<strong>Bundle upload failed</strong><br/><pre><code>%s</code></pre>", e.getMessage())); + "<strong>Bundle upload failed</strong><br/><pre><code>%s</code></pre>", sanitizedMessage)); } finally { if (!file.delete()) { log(Level.WARNING, "Could not delete bundle {0}" + file);
src/test/java/com/cloudbees/jenkins/plugins/advisor/BundleUploadTest.java+18 −0 modified@@ -153,6 +153,24 @@ void getInitialDelay() { is(equalTo(TimeUnit.MINUTES.toMillis(BundleUpload.INITIAL_DELAY_MINUTES)))); } + @WithTimeout(30) + @Test + void protectsAgainstRogueServer(JenkinsRule j) { + var config = AdvisorGlobalConfiguration.getInstance(); + config.setEmail(TEST_EMAIL); + config.setAcceptToS(true); + + wireMock.stubFor(get(urlEqualTo("/api/health")).willReturn(aResponse().withStatus(200))); + wireMock.stubFor(post(format( + "/api/users/%s/upload/%s", TEST_EMAIL, j.getInstance().getLegacyInstanceId())) + .willReturn(aResponse().withStatus(201).withBody("<img/src/onerror=alert(1)>"))); + + runBundleUpload(j); + + // The stored response is properly escaped + assertThat(config.getLastBundleResult(), containsString("<code>201 - <img/src/onerror=alert(1)></code>")); + } + /** * Runs the {@link BundleUpload} task and waits for it to finish. */
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
1- Jenkins Security Advisory 2025-05-14Jenkins Security Advisories · May 14, 2025