[SECURITY-3669]

This commit is contained in:
Daniel Beck authored and Jenkins CERT CI committed 2026-02-15 03:32:45 +00:00
1 parent f92eadb581
commit 578c028e2c
3 files changed
+162 -1

No files matched your search

@@ -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 {
@@ -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;")));
}
}
}
@@ -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>