From 578c028e2cdfdc9e124d0ca389a80bb2bd231ab2 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Sun, 15 Feb 2026 03:32:45 +0000 Subject: [PATCH] [SECURITY-3669] --- .../main/java/hudson/slaves/OfflineCause.java | 8 +- .../jenkins/security/Security3669Test.java | 122 ++++++++++++++++++ .../dataFromDisk/nodes/a1/config.xml | 33 +++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 test/src/test/java/jenkins/security/Security3669Test.java create mode 100644 test/src/test/resources/jenkins/security/Security3669Test/dataFromDisk/nodes/a1/config.xml diff --git a/core/src/main/java/hudson/slaves/OfflineCause.java b/core/src/main/java/hudson/slaves/OfflineCause.java index 07c2d3af28..b88524a70a 100644 --- a/core/src/main/java/hudson/slaves/OfflineCause.java +++ b/core/src/main/java/hudson/slaves/OfflineCause.java @@ -26,6 +26,7 @@ package hudson.slaves; 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 abstract class OfflineCause implements IOfflineCause { * @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 abstract class OfflineCause implements IOfflineCause { public String getIcon() { return "symbol-person"; } + + @Override + public String toString() { + return Util.escape(super.toString()); + } } public static class ByCLI extends UserCause { diff --git a/test/src/test/java/jenkins/security/Security3669Test.java b/test/src/test/java/jenkins/security/Security3669Test.java new file mode 100644 index 0000000000..01bd388031 --- /dev/null +++ b/test/src/test/java/jenkins/security/Security3669Test.java @@ -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(""); + 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("")), + 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(""); + 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("")), + containsString("Disconnected by anonymous : <img src=x onerror=alert(1)>"))); + } + } + + @Test + void postConfigXmlWithLocalizable(JenkinsRule jenkinsRule) throws Exception { + final DumbSlave agent = jenkinsRule.createOnlineSlave(); + + String xml = "\n" + + "\n" + + " \n" + + " 1770000000000\n" + + " \n" + + " \n" + + " hudson.slaves.Messages\n" + + " \n" + + " SlaveComputer.DisconnectedBy\n" + + " \n" + + " admin\n" + + " : <img src=x onerror=alert(1)>\n" + + " \n" + + " \n" + + " admin\n" + + " <img src=x onerror=alert(1)>\n" + + " \n" + + " " + agent.getNodeName() + "\n" + + " \n" + + " /tmp/foo\n" + + " 1\n" + + " NORMAL\n" + + " \n" + + " \n" + + " \n" + + " false\n" + + " remoting\n" + + " false\n" + + " \n" + + " false\n" + + " \n" + + " \n" + + " \n" + + ""; + 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("")), + 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("")), + containsString("Disconnected by anonymous : <img src=x onerror=alert(1)>"))); + } + } +} diff --git a/test/src/test/resources/jenkins/security/Security3669Test/dataFromDisk/nodes/a1/config.xml b/test/src/test/resources/jenkins/security/Security3669Test/dataFromDisk/nodes/a1/config.xml new file mode 100644 index 0000000000..3e1ca591f9 --- /dev/null +++ b/test/src/test/resources/jenkins/security/Security3669Test/dataFromDisk/nodes/a1/config.xml @@ -0,0 +1,33 @@ + + + + 1770000000000 + + + hudson.slaves.Messages + + SlaveComputer.DisconnectedBy + + anonymous + : <img src=x onerror=alert(1)> + + + <img src=x onerror=alert(1)> + + a1 + + /tmp/a1 + 1 + NORMAL + + + + false + remoting + false + + false + + + + \ No newline at end of file