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.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.main:jenkins-coreMaven | >= 2.542, < 2.551 | 2.551 |
org.jenkins-ci.main:jenkins-coreMaven | >= 2.483, < 2.541.2 | 2.541.2 |
Affected products
2- Range: >=2.483, <=2.550; >=2.492.1, <=2.541.1 (LTS)
- Jenkins Project/Jenkinsv5Range: 0
Patches
13 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 : <img src=x onerror=alert(1)>"))); + } + } + + @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 : <img src=x onerror=alert(1)>"))); + } + } + + @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> : <img src=x onerror=alert(1)></string>\n" + + " </args>\n" + + " </description>\n" + + " <userId>admin</userId>\n" + + " <message><img src=x onerror=alert(1)></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 : <img src=x onerror=alert(1)>"))); + } + } + + @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 : <img src=x onerror=alert(1)>"))); + } + } +}
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> : <img src=x onerror=alert(1)></string> + </args> + </description> + <message><img src=x onerror=alert(1)></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- github.com/advisories/GHSA-85h6-5m3v-gx37ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27099ghsaADVISORY
- www.jenkins.io/security/advisory/2026-02-18/ghsavendor-advisoryWEB
- github.com/jenkinsci/jenkins/commit/578c028e2cdfdc9e124d0ca389a80bb2bd231ab2ghsaWEB
- github.com/jenkinsci/jenkins/releases/tag/jenkins-2.541.2ghsaWEB
- github.com/jenkinsci/jenkins/releases/tag/jenkins-2.551ghsaWEB
News mentions
1- Jenkins Security Advisory 2026-02-18Jenkins Security Advisories · Feb 18, 2026