Jenkins: Stored XSS vulnerability in node offline cause description
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
2.483 → fixed in 2.568
Details
Jenkins 2.483 through 2.567 (both inclusive), LTS 2.492.1 through 2.555.2 (both inclusive) does not escape the user-provided description of a generic offline cause that could be set through the `POST config.xml` API, resulting in a stored cross-site scripting (XSS) vulnerability exploitable by attackers with Agent/Configure permission.
The fix
[SECURITY-3731]
core/src/main/java/hudson/slaves/OfflineCause.java+0 −5
@@ -203,11 +203,6 @@ public String getComputerIconAltText() {public String getIcon() {return "symbol-person";}--@Override-public String toString() {-return Util.escape(super.toString());-}}public static class ByCLI extends UserCause {
core/src/main/resources/hudson/slaves/OfflineCause/cause.jelly+1 −1
@@ -29,7 +29,7 @@ THE SOFTWARE.<i:formatDate value="${it.time}" type="both" dateStyle="medium" timeStyle="medium"/></div><div class="message">-<j:out value="${it}" />+${it}</div></div></j:jelly>
test/src/test/java/jenkins/security/Security3669Test.java+49 −0
@@ -15,6 +15,7 @@import org.htmlunit.html.HtmlFormUtil;import org.htmlunit.html.HtmlPage;import org.junit.jupiter.api.Test;+import org.jvnet.hudson.test.Issue;import org.jvnet.hudson.test.JenkinsRule;import org.jvnet.hudson.test.junit.jupiter.WithJenkins;import org.jvnet.hudson.test.junit.jupiter.WithLocalData;@@ -104,6 +105,54 @@ void postConfigXmlWithLocalizable(JenkinsRule jenkinsRule) throws Exception {}}+@Test+@Issue("SECURITY-3731")+void postConfigXmlWithNonUserCause(JenkinsRule jenkinsRule) throws Exception {+final DumbSlave agent = jenkinsRule.createOnlineSlave();++String xml = "<?xml version='1.0' encoding='UTF-8'?>\n" ++"<slave>\n" ++" <temporaryOfflineCause class=\"hudson.slaves.OfflineCause$SimpleOfflineCause\">\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" ++" </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("<img src=x onerror=alert(1)>")));+}+}+@Test@WithLocalDatavoid dataFromDisk(JenkinsRule jenkinsRule) throws Exception {