Security context
Medium· 5.4GHSA-9qgf-4fpf-cmh2 CVE-2019-10404CWE-79Published May 24, 2022

Improper Neutralization of Input During Web Page Generation in Jenkins

Research this vulnerability

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

0 → fixed in 2.176.42.177 → fixed in 2.197

Details

Jenkins 2.196 and earlier, LTS 2.176.3 and earlier did not escape the reason why a queue items is blcoked in tooltips, resulting in a stored XSS vulnerability exploitable by users able to control parts of the reason a queue item is blocked, such as label expressions not matching any idle executors.

The fix

[SECURITY-1537]

Wadeck Follonier· Sep 10, 2019, 06:30 PM+21528c8d014330
core/src/main/resources/hudson/scm/AbstractScmTagAction/badge.jelly+1 1
@@ -25,6 +25,6 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:if xmlns:j="jelly:core" xmlns:l="/lib/layout" test="${it.isTagged()}">
<a href="${rootURL}/${it.run.url}tagBuild/">
- <l:icon class="icon-save icon-sm" tooltip="${it.tooltip}"/>
+ <l:icon class="icon-save icon-sm" tooltip="${h.escape(it.tooltip)}"/>
</a>
</j:if>
core/src/main/resources/lib/hudson/queue.jelly+1 1
@@ -74,7 +74,7 @@ THE SOFTWARE.
<j:set var="stuck" value="${item.isStuck()}"/>
<j:choose>
<j:when test="${h.hasPermission(item.task,item.task.READ)}">
- <a href="${rootURL}/${item.task.url}" class="model-link inside tl-tr" tooltip="${h.escape(item.causesDescription)}${item.why}${h.escape(item.params)}&lt;br&gt;${%WaitingFor(item.inQueueForString)}">
+ <a href="${rootURL}/${item.task.url}" class="model-link inside tl-tr" tooltip="${h.escape(item.causesDescription)}${h.escape(item.why)}${h.escape(item.params)}&lt;br&gt;${%WaitingFor(item.inQueueForString)}">
<l:breakable value="${item.task.fullDisplayName}"/>
</a>
<!-- TODO include estimated number as in BuildHistoryWidget/entries.jelly if possible -->
test/src/test/java/hudson/model/QueueSEC1537Test.java+66 0
@@ -0,0 +1,66 @@
+package hudson.model;
+
+import com.gargoylesoftware.htmlunit.html.DomElement;
+import com.gargoylesoftware.htmlunit.html.DomNodeList;
+import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
+import com.gargoylesoftware.htmlunit.html.HtmlElement;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import hudson.slaves.NodeProvisionerRule;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+//TODO merge into QueueTest after security patch
+public class QueueSEC1537Test {
+
+ @Rule
+ public JenkinsRule r = new NodeProvisionerRule(-1, 0, 10);
+
+ @Test
+ @Issue("SECURITY-1537")
+ public void regularTooltipDisplayedCorrectly() throws Exception {
+ FreeStyleProject p = r.createFreeStyleProject();
+
+ String expectedLabel = "\"expected label\"";
+ p.setAssignedLabel(Label.get(expectedLabel));
+
+ p.scheduleBuild2(0);
+
+ String tooltip = buildAndExtractTooltipAttribute();
+ assertThat(tooltip, containsString(expectedLabel.substring(1, expectedLabel.length() - 1)));
+ }
+
+ @Test
+ @Issue("SECURITY-1537")
+ public void preventXssInCauseOfBlocking() throws Exception {
+ FreeStyleProject p = r.createFreeStyleProject();
+ p.setAssignedLabel(Label.get("\"<img/src='x' onerror=alert(123)>xss\""));
+
+ p.scheduleBuild2(0);
+
+ String tooltip = buildAndExtractTooltipAttribute();
+ assertThat(tooltip, not(containsString("<img")));
+ assertThat(tooltip, containsString("&lt;"));
+ }
+
+ private String buildAndExtractTooltipAttribute() throws Exception {
+ JenkinsRule.WebClient wc = r.createWebClient();
+
+ HtmlPage page = wc.goTo("");
+
+ DomElement buildQueue = page.getElementById("buildQueue");
+ DomNodeList<HtmlElement> anchors = buildQueue.getElementsByTagName("a");
+ HtmlAnchor anchorWithTooltip = (HtmlAnchor) anchors.stream()
+ .filter(a -> StringUtils.isNotEmpty(a.getAttribute("tooltip")))
+ .findFirst().orElseThrow(IllegalStateException::new);
+
+ String tooltip = anchorWithTooltip.getAttribute("tooltip");
+ return tooltip;
+ }
+}
test/src/test/java/hudson/scm/AbstractScmTagActionTest.java+147 0
@@ -0,0 +1,147 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2019, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.scm;
+
+import com.gargoylesoftware.htmlunit.html.DomElement;
+import com.gargoylesoftware.htmlunit.html.DomNodeList;
+import com.gargoylesoftware.htmlunit.html.HtmlElement;
+import com.gargoylesoftware.htmlunit.html.HtmlImage;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import hudson.FilePath;
+import hudson.Launcher;
+import hudson.model.FreeStyleProject;
+import hudson.model.Run;
+import hudson.model.TaskListener;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class AbstractScmTagActionTest {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Test
+ public void regularTextDisplayedCorrectly() throws Exception {
+ FreeStyleProject p = j.createFreeStyleProject();
+
+ String tagToKeep = "Nice tag with space";
+ p.setScm(new FakeSCM(tagToKeep));
+
+ j.assertBuildStatusSuccess(p.scheduleBuild2(0));
+
+ String tooltip = buildAndExtractTooltipAttribute(p);
+ assertEquals(tagToKeep, tooltip);
+ }
+
+ @Test
+ @Issue("SECURITY-1537")
+ public void preventXssInTagAction() throws Exception {
+ FreeStyleProject p = j.createFreeStyleProject();
+ p.setScm(new FakeSCM("<img src='x' onerror=alert(123)>XSS"));
+
+ j.assertBuildStatusSuccess(p.scheduleBuild2(0));
+
+ String tooltip = buildAndExtractTooltipAttribute(p);
+ assertThat(tooltip, not(containsString("<")));
+ assertThat(tooltip, startsWith("&lt;"));
+ }
+
+ private String buildAndExtractTooltipAttribute(FreeStyleProject p) throws Exception {
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.getPage(p);
+
+ DomElement buildHistory = page.getElementById("buildHistory");
+ DomNodeList<HtmlElement> imgs = buildHistory.getElementsByTagName("img");
+ HtmlImage tagImage = (HtmlImage) imgs.stream()
+ .filter(i -> i.getAttribute("class").contains("icon-save"))
+ .findFirst().orElseThrow(IllegalStateException::new);
+
+ String tooltip = tagImage.getAttribute("tooltip");
+ return tooltip;
+ }
+
+ public static class FakeSCM extends SCM {
+
+ private String desiredTooltip;
+
+ FakeSCM(String desiredTooltip) {
+ this.desiredTooltip = desiredTooltip;
+ }
+
+ @Override
+ public ChangeLogParser createChangeLogParser() {
+ return null;
+ }
+
+ @Override
+ public void checkout(@Nonnull Run<?, ?> build, @Nonnull Launcher launcher, @Nonnull FilePath workspace, @Nonnull TaskListener listener, @CheckForNull File changelogFile, @CheckForNull SCMRevisionState baseline) throws IOException, InterruptedException {
+ build.addAction(new TooltipTagAction(build, desiredTooltip));
+ }
+ }
+
+ public static class TooltipTagAction extends AbstractScmTagAction {
+
+ private String desiredTooltip;
+
+ TooltipTagAction(Run build, String desiredTooltip) {
+ super(build);
+ this.desiredTooltip = desiredTooltip;
+ }
+
+ @Override
+ public boolean isTagged() {
+ return true;
+ }
+
+ @Override
+ public String getIconFileName() {
+ return "save.gif";
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "ClickOnMe";
+ }
+
+ @Override
+ public String getTooltip() {
+ return desiredTooltip;
+ }
+ }
+}

References