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