VYPR
High severityNVD Advisory· Published May 14, 2025· Updated May 20, 2025

CVE-2025-47885

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.

PackageAffected versionsPatched versions
org.jenkins-ci.plugins:cloudbees-jenkins-advisorMaven
< 374.376.v3a_41a_a_142efe374.376.v3a_41a_a_142efe

Affected products

2

Patches

1
4b456b3110d1

SECURITY-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 - &lt;img/src/onerror=alert(1)&gt;</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

4

News mentions

1