VYPR
High severityNVD Advisory· Published Feb 18, 2026· Updated Feb 18, 2026

CVE-2026-27099

CVE-2026-27099

Description

Jenkins 2.483 through 2.550 (both inclusive), LTS 2.492.1 through 2.541.1 (both inclusive) does not escape the user-provided description of the "Mark temporarily offline" offline cause, resulting in a stored cross-site scripting (XSS) vulnerability exploitable by attackers with Agent/Configure or Agent/Disconnect permission.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Jenkins 2.483 through 2.550 and LTS 2.492.1 through 2.541.1 have a stored XSS vulnerability via the unescaped 'Mark temporarily offline' cause description.

Vulnerability

Overview

Jenkins versions 2.483 through 2.550 (weekly) and LTS 2.492.1 through 2.541.1 (LTS) do not escape the user-provided description of the 'Mark temporarily offline' offline cause. This results in a stored cross-site scripting (XSS) vulnerability [1][4]) vulnerability. The root cause is that the description is defined as containing HTML and rendered as such without proper sanitization [4].

Exploitation

An attacker with Agent/Configure or Agent/Disconnect permission can exploit this vulnerability by providing a malicious payload in the offline cause description [1][4]. The payload is stored and executed when the description is rendered, for example on the node page. The commit fixing this issue includes a test that demonstrates injecting ` and verifying it is escaped to <img src=x onerror=alert(1)>` [2].

Impact

Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of a victim's browser session. This can lead to session hijacking, credential theft, or other actions the victim user can perform. The vulnerability is rated High severity [4].

Mitigation

Jenkins has released versions 2.551 (weekly) and 2.541.2 (LTS) that escape the user-provided description [4]. Users should upgrade to these versions or later. No workaround is mentioned in the advisory.

AI Insight generated on May 19, 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.main:jenkins-coreMaven
>= 2.542, < 2.5512.551
org.jenkins-ci.main:jenkins-coreMaven
>= 2.483, < 2.541.22.541.2

Affected products

2
  • Range: >=2.483, <=2.550; >=2.492.1, <=2.541.1 (LTS)
  • Jenkins Project/Jenkinsv5
    Range: 0

Patches

1
578c028e2cdf

[SECURITY-3669]

https://github.com/jenkinsci/jenkinsDaniel BeckFeb 15, 2026via ghsa
3 files changed · +162 1
  • core/src/main/java/hudson/slaves/OfflineCause.java+7 1 modified
    @@ -26,6 +26,7 @@
     
     import edu.umd.cs.findbugs.annotations.CheckForNull;
     import edu.umd.cs.findbugs.annotations.NonNull;
    +import hudson.Util;
     import hudson.model.Computer;
     import hudson.model.User;
     import java.io.ObjectStreamException;
    @@ -166,7 +167,7 @@ public User getUser() {
              * @return the message that was provided when the computer was taken offline
              */
             public String getMessage() {
    -            return message;
    +            return Util.escape(message);
             }
     
             // Storing the User in a filed was a mistake, switch to userId
    @@ -202,6 +203,11 @@ public String getComputerIconAltText() {
             public String getIcon() {
                 return "symbol-person";
             }
    +
    +        @Override
    +        public String toString() {
    +            return Util.escape(super.toString());
    +        }
         }
     
         public static class ByCLI extends UserCause {
    
  • test/src/test/java/jenkins/security/Security3669Test.java+122 0 added
    @@ -0,0 +1,122 @@
    +package jenkins.security;
    +
    +import static org.hamcrest.MatcherAssert.assertThat;
    +import static org.hamcrest.Matchers.allOf;
    +import static org.hamcrest.Matchers.containsString;
    +import static org.hamcrest.Matchers.not;
    +import static org.hamcrest.Matchers.nullValue;
    +
    +import hudson.model.Computer;
    +import hudson.model.User;
    +import hudson.slaves.DumbSlave;
    +import hudson.slaves.OfflineCause;
    +import java.io.ByteArrayInputStream;
    +import jenkins.model.Jenkins;
    +import org.htmlunit.html.HtmlFormUtil;
    +import org.htmlunit.html.HtmlPage;
    +import org.junit.jupiter.api.Test;
    +import org.jvnet.hudson.test.JenkinsRule;
    +import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
    +import org.jvnet.hudson.test.junit.jupiter.WithLocalData;
    +
    +@WithJenkins
    +public class Security3669Test {
    +    @Test
    +    public void newOfflineCause(JenkinsRule jenkinsRule) throws Exception {
    +        try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
    +            HtmlPage formPage = webClient.getPage(Jenkins.get(), "markOffline");
    +            formPage.getElementByName("offlineMessage").setTextContent("<img src=x onerror=alert(1)>");
    +            HtmlFormUtil.submit(formPage.getForms().stream().filter(f -> f.getActionAttribute().equals("toggleOffline")).findFirst().orElseThrow());
    +
    +            final HtmlPage nodePage = webClient.getPage(Jenkins.get());
    +            assertThat(
    +                    nodePage.getWebResponse().getContentAsString(),
    +                    allOf(
    +                            not(containsString("<img src=x onerror=alert(1)>")),
    +                            containsString("Disconnected by anonymous : &lt;img src=x onerror=alert(1)&gt;")));
    +        }
    +    }
    +
    +    @Test
    +    public void editOfflineCause(JenkinsRule jenkinsRule) throws Exception {
    +        Jenkins.get().getComputer("").setTemporaryOfflineCause(new OfflineCause.UserCause(User.current(), "initial reason"));
    +        try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
    +            HtmlPage formPage = webClient.getPage(Jenkins.get(), "setOfflineCause");
    +            formPage.getElementByName("offlineMessage").setTextContent("<img src=x onerror=alert(1)>");
    +            HtmlFormUtil.submit(formPage.getForms().stream().filter(f -> f.getActionAttribute().equals("changeOfflineCause")).findFirst().orElseThrow());
    +
    +            final HtmlPage nodePage = webClient.getPage(Jenkins.get());
    +            assertThat(
    +                    nodePage.getWebResponse().getContentAsString(),
    +                    allOf(
    +                            not(containsString("<img src=x onerror=alert(1)>")),
    +                            containsString("Disconnected by anonymous : &lt;img src=x onerror=alert(1)&gt;")));
    +        }
    +    }
    +
    +    @Test
    +    void postConfigXmlWithLocalizable(JenkinsRule jenkinsRule) throws Exception {
    +        final DumbSlave agent = jenkinsRule.createOnlineSlave();
    +
    +        String xml = "<?xml version=\"1.1\" encoding=\"UTF-8\"?>\n" +
    +                "<slave>\n" +
    +                "  <temporaryOfflineCause class=\"hudson.slaves.OfflineCause$UserCause\">\n" +
    +                "    <timestamp>1770000000000</timestamp>\n" +
    +                "    <description>\n" +
    +                "      <holder>\n" +
    +                "        <owner>hudson.slaves.Messages</owner>\n" +
    +                "      </holder>\n" +
    +                "      <key>SlaveComputer.DisconnectedBy</key>\n" +
    +                "      <args>\n" +
    +                "        <string>admin</string>\n" +
    +                "        <string> : &lt;img src=x onerror=alert(1)&gt;</string>\n" +
    +                "      </args>\n" +
    +                "    </description>\n" +
    +                "    <userId>admin</userId>\n" +
    +                "    <message>&lt;img src=x onerror=alert(1)&gt;</message>\n" +
    +                "  </temporaryOfflineCause>\n" +
    +                "  <name>" + agent.getNodeName() + "</name>\n" +
    +                "  <description></description>\n" +
    +                "  <remoteFS>/tmp/foo</remoteFS>\n" +
    +                "  <numExecutors>1</numExecutors>\n" +
    +                "  <mode>NORMAL</mode>\n" +
    +                "  <retentionStrategy class=\"hudson.slaves.RetentionStrategy$Always\"/>\n" +
    +                "  <launcher class=\"hudson.slaves.JNLPLauncher\">\n" +
    +                "    <workDirSettings>\n" +
    +                "      <disabled>false</disabled>\n" +
    +                "      <internalDir>remoting</internalDir>\n" +
    +                "      <failIfWorkDirIsMissing>false</failIfWorkDirIsMissing>\n" +
    +                "    </workDirSettings>\n" +
    +                "    <webSocket>false</webSocket>\n" +
    +                "  </launcher>\n" +
    +                "  <label></label>\n" +
    +                "  <nodeProperties/>\n" +
    +                "</slave>";
    +        agent.toComputer().updateByXml(new ByteArrayInputStream(xml.getBytes()));
    +
    +        try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
    +            final HtmlPage nodePage = webClient.getPage(agent);
    +            assertThat(
    +                    nodePage.getWebResponse().getContentAsString(),
    +                    allOf(
    +                            not(containsString("<img src=x onerror=alert(1)>")),
    +                            containsString("Disconnected by admin : &lt;img src=x onerror=alert(1)&gt;")));
    +        }
    +    }
    +
    +    @Test
    +    @WithLocalData
    +    void dataFromDisk(JenkinsRule jenkinsRule) throws Exception {
    +        final Computer agent = jenkinsRule.jenkins.getComputer("a1");
    +        assertThat(agent, not(nullValue()));
    +
    +        try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
    +            final HtmlPage nodePage = webClient.getPage(agent.getNode());
    +            assertThat(
    +                    nodePage.getWebResponse().getContentAsString(),
    +                    allOf(
    +                            not(containsString("<img src=x onerror=alert(1)>")),
    +                            containsString("Disconnected by anonymous : &lt;img src=x onerror=alert(1)&gt;")));
    +        }
    +    }
    +}
    
  • test/src/test/resources/jenkins/security/Security3669Test/dataFromDisk/nodes/a1/config.xml+33 0 added
    @@ -0,0 +1,33 @@
    +<?xml version='1.1' encoding='UTF-8'?>
    +<slave>
    +  <temporaryOfflineCause class="hudson.slaves.OfflineCause$UserCause">
    +    <timestamp>1770000000000</timestamp>
    +    <description>
    +      <holder>
    +        <owner>hudson.slaves.Messages</owner>
    +      </holder>
    +      <key>SlaveComputer.DisconnectedBy</key>
    +      <args>
    +        <string>anonymous</string>
    +        <string> : &lt;img src=x onerror=alert(1)&gt;</string>
    +      </args>
    +    </description>
    +    <message>&lt;img src=x onerror=alert(1)&gt;</message>
    +  </temporaryOfflineCause>
    +  <name>a1</name>
    +  <description></description>
    +  <remoteFS>/tmp/a1</remoteFS>
    +  <numExecutors>1</numExecutors>
    +  <mode>NORMAL</mode>
    +  <retentionStrategy class="hudson.slaves.RetentionStrategy$Always"/>
    +  <launcher class="hudson.slaves.JNLPLauncher">
    +    <workDirSettings>
    +      <disabled>false</disabled>
    +      <internalDir>remoting</internalDir>
    +      <failIfWorkDirIsMissing>false</failIfWorkDirIsMissing>
    +    </workDirSettings>
    +    <webSocket>false</webSocket>
    +  </launcher>
    +  <label></label>
    +  <nodeProperties/>
    +</slave>
    \ No newline at end of file
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

1