Improper Neutralization of Input During Web Page Generation in Jenkins
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]
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)}<br>${%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)}<br>${%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("<"));+}++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("<"));+}++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;+}+}+}