CVE-2018-1000170
Description
Jenkins 2.115 and older, LTS 2.107.1 and older, has a stored XSS in confirmation dialogs allowing attackers with Job/Configure or Job/Create permission to execute JavaScript in other users' browsers.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins 2.115 and older, LTS 2.107.1 and older, has a stored XSS in confirmation dialogs allowing attackers with Job/Configure or Job/Create permission to execute JavaScript in other users' browsers.
Vulnerability
A cross-site scripting (XSS) vulnerability exists in Jenkins weekly up to and including 2.115 and Jenkins LTS up to and including 2.107.1 [3]. The flaw is located in the confirmationList.jelly and stopButton.jelly views, which fail to escape item names when rendering JavaScript confirmation dialogs [1]. An attacker with Job/Configure or Job/Create permission can create or rename a job with a name containing malicious JavaScript [1].
Exploitation
The attacker must have Job/Configure or Job/Create permission in Jenkins [1]. The attacker sets the name of a job or item to include JavaScript code (e.g., via the job configuration UI). When another user (including administrators) performs certain UI actions that trigger a confirmation dialog (such as deleting or stopping the job), the dialog displays the item name without proper escaping. The injected JavaScript executes in the context of the victim's browser session [1][3]. No additional user interaction beyond viewing the confirmation dialog is required for the XSS to fire.
Impact
Successful exploitation results in arbitrary JavaScript execution in the victim's browser within the Jenkins web UI. The attacker may steal session cookies, perform actions on behalf of the victim, modify Jenkins configuration, or extract sensitive information. The scope is limited to the Jenkins domain and the privileges of the victim user [1][3].
Mitigation
Jenkins weekly users should upgrade to version 2.116 or later; Jenkins LTS users should upgrade to version 2.107.2 or later [3]. The fix, introduced via commit 07d18cf [2], properly escapes item names in confirmation dialogs. There is no known workaround for unpatched versions. This CVE is not listed on CISA's Known Exploited Vulnerabilities catalog as of the publication date.
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.main:jenkins-coreMaven | >= 2.108, < 2.116 | 2.116 |
org.jenkins-ci.main:jenkins-coreMaven | < 2.107.2 | 2.107.2 |
Affected products
1Patches
113 files changed · +761 −39
core/src/main/resources/lib/form/expandableTextbox.jelly+1 −1 modified@@ -75,7 +75,7 @@ THE SOFTWARE. <j:mute>${customizedFields.add(name)}</j:mute> </j:if> </td><td width="1"> - <input type="button" value="▼" onclick="expandTextArea(this,'textarea.${name}')" + <input type="button" value="▼" onclick="expandTextArea(this,'textarea.${h.jsStringEscape(name)}')" tooltip="${%tooltip}"/> </td> </tr>
core/src/main/resources/lib/form/validateButton.jelly+1 −1 modified@@ -48,7 +48,7 @@ THE SOFTWARE. </st:documentation> <f:entry> <div style="float:right"> - <input type="button" value="${title}" class="yui-button validate-button" onclick="validateButton('${descriptor.descriptorFullUrl}/${method}','${with}',this)" /> + <input type="button" value="${title}" class="yui-button validate-button" onclick="validateButton('${descriptor.descriptorFullUrl}/${h.jsStringEscape(method)}','${h.jsStringEscape(with)}',this)" /> </div> <div style="display:none;"> <img src="${imagesURL}/spinner.gif" /> ${attrs.progress}
core/src/main/resources/lib/layout/confirmationLink.jelly+2 −2 modified@@ -31,7 +31,7 @@ THE SOFTWARE. <st:attribute name="href" use="required"> The URL to go to. </st:attribute> - <st:attribute name="post"> + <st:attribute name="post" type="boolean"> Use POST rather than GET (recommended). </st:attribute> <st:attribute name="message" use="required"> @@ -42,7 +42,7 @@ THE SOFTWARE. </st:attribute> </st:documentation> <j:set var="id" value="${h.generateId()}"/> - <a href="#" class="${class}" onclick="confirmPOST_${id}(${post ?: 'false'}, '${attrs.href}', '${h.jsStringEscape(message)}')"><d:invokeBody/></a> + <a href="#" class="${class}" onclick="confirmPOST_${id}(${post ? 'true' : 'false'}, '${h.jsStringEscape(attrs.href)}', '${h.jsStringEscape(message)}')"><d:invokeBody/></a> <script> function confirmPOST_${id}(post, href, message) { if (confirm(message)) {
core/src/main/resources/lib/layout/stopButton.jelly+4 −4 modified@@ -39,13 +39,13 @@ THE SOFTWARE. </st:documentation> <j:choose> <j:when test="${confirm == null}"> - <a class="stop-button-link" href="${href}" onclick='new Ajax.Request("${href}"); return false;'> - <l:icon class="icon-stop icon-sm"/> + <a class="stop-button-link" href="${href}" onclick='new Ajax.Request("${h.jsStringEscape(href)}"); return false;'> + <l:icon class="icon-stop icon-sm" alt="${alt}"/> </a> </j:when> <j:otherwise> - <a class="stop-button-link" href="${href}" onclick='if(confirm("${confirm}"))new Ajax.Request("${href}"); return false;'> - <l:icon class="icon-stop icon-sm"/> + <a class="stop-button-link" href="${href}" onclick='if(confirm("${h.jsStringEscape(confirm)}"))new Ajax.Request("${h.jsStringEscape(href)}"); return false;'> + <l:icon class="icon-stop icon-sm" alt="${alt}"/> </a> </j:otherwise> </j:choose>
test/src/test/java/lib/form/ExpandableTextboxTest.java+96 −5 modified@@ -24,39 +24,130 @@ package lib.form; import static com.gargoylesoftware.htmlunit.HttpMethod.POST; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.html.DomNodeList; +import com.gargoylesoftware.htmlunit.html.HtmlAnchor; +import com.gargoylesoftware.htmlunit.html.HtmlButton; +import com.gargoylesoftware.htmlunit.html.HtmlButtonInput; +import com.gargoylesoftware.htmlunit.html.HtmlElement; +import com.gargoylesoftware.htmlunit.html.HtmlElementUtil; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.model.UnprotectedRootAction; +import hudson.util.HttpResponses; +import lib.layout.ConfirmationLinkTest; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.HudsonTestCase; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.WebMethod; import org.w3c.dom.NodeList; +import javax.annotation.CheckForNull; + /** * @author Kohsuke Kawaguchi */ -public class ExpandableTextboxTest extends HudsonTestCase { +public class ExpandableTextboxTest { + @Rule + public JenkinsRule j = new JenkinsRule(); + @Issue("JENKINS-2816") + @Test public void testMultiline() throws Exception { // because attribute values are normalized, it's not very easy to encode multi-line string as @value. So let's use the system message here. - jenkins.setSystemMessage("foo\nbar\nzot"); + j.jenkins.setSystemMessage("foo\nbar\nzot"); HtmlPage page = evaluateAsHtml("<l:layout><l:main-panel><table><j:set var='instance' value='${it}'/><f:expandableTextbox field='systemMessage' /></table></l:main-panel></l:layout>"); // System.out.println(page.getWebResponse().getContentAsString()); NodeList textareas = page.getElementsByTagName("textarea"); assertEquals(1, textareas.getLength()); - assertEquals(jenkins.getSystemMessage(),textareas.item(0).getTextContent()); + assertEquals(j.jenkins.getSystemMessage(),textareas.item(0).getTextContent()); } /** * Evaluates the literal Jelly script passed as a parameter as HTML and returns the page. */ protected HtmlPage evaluateAsHtml(String jellyScript) throws Exception { - HudsonTestCase.WebClient wc = new WebClient(); - + JenkinsRule.WebClient wc = j.createWebClient(); WebRequest req = new WebRequest(wc.createCrumbedUrl("eval"), POST); req.setEncodingType(null); req.setRequestBody("<j:jelly xmlns:j='jelly:core' xmlns:st='jelly:stapler' xmlns:l='/lib/layout' xmlns:f='/lib/form'>"+jellyScript+"</j:jelly>"); Page page = wc.getPage(req); return (HtmlPage) page; } + + @Test + public void noInjectionArePossible() throws Exception { + TestRootAction testParams = j.jenkins.getExtensionList(UnprotectedRootAction.class).get(TestRootAction.class); + assertNotNull(testParams); + + checkRegularCase(testParams); + checkInjectionInName(testParams); + } + + private void checkRegularCase(TestRootAction testParams) throws Exception { + testParams.paramName = "testName"; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + HtmlElementUtil.click(getExpandButton(p)); + assertNotEquals("hacked", p.getTitleText()); + } + + private void checkInjectionInName(TestRootAction testParams) throws Exception { + testParams.paramName = "testName',document.title='hacked'+'"; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + HtmlElementUtil.click(getExpandButton(p)); + assertNotEquals("hacked", p.getTitleText()); + } + + private HtmlButtonInput getExpandButton(HtmlPage page){ + DomNodeList<HtmlElement> buttons = page.getElementById("test-panel").getElementsByTagName("input"); + // the first one is the text input + assertEquals(2, buttons.size()); + return (HtmlButtonInput) buttons.get(1); + } + + @TestExtension("noInjectionArePossible") + public static final class TestRootAction implements UnprotectedRootAction { + + public String paramName; + + @Override + public @CheckForNull String getIconFileName() { + return null; + } + + @Override + public @CheckForNull String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "test"; + } + + @WebMethod(name = "submit") + public HttpResponse doSubmit(StaplerRequest request) { + return HttpResponses.plainText("method:" + request.getMethod()); + } + } }
test/src/test/java/lib/form/ValidateButtonTest.java+157 −25 modified@@ -23,53 +23,185 @@ */ package lib.form; +import com.gargoylesoftware.htmlunit.html.DomNodeList; import com.gargoylesoftware.htmlunit.html.HtmlButton; +import com.gargoylesoftware.htmlunit.html.HtmlElement; import com.gargoylesoftware.htmlunit.html.HtmlElementUtil; import com.gargoylesoftware.htmlunit.html.HtmlFormUtil; -import org.jvnet.hudson.test.HudsonTestCase; +import hudson.model.UnprotectedRootAction; +import jenkins.model.Jenkins; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; import org.kohsuke.stapler.QueryParameter; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.Extension; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import org.kohsuke.stapler.StaplerRequest; + +import javax.annotation.CheckForNull; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; /** * * * @author Kohsuke Kawaguchi */ -public class ValidateButtonTest extends HudsonTestCase implements Describable<ValidateButtonTest> { +public class ValidateButtonTest { + @Rule + public JenkinsRule j = new JenkinsRule(); - public void test1() throws Exception { - DescriptorImpl d = getDescriptor(); + @Test + public void testValidateIsCalled() throws Exception { + TestValidateIsCalled.DescriptorImpl d = j.jenkins.getDescriptorByType(TestValidateIsCalled.DescriptorImpl.class); + assertNotNull(d); + d.test1Outcome = new Exception(); // if doValidateTest1() doesn't get invoked, we want to know. - HtmlPage p = createWebClient().goTo("self/test1"); + HtmlPage p = j.createWebClient().goTo("test"); HtmlButton button = HtmlFormUtil.getButtonByCaption(p.getFormByName("config"), "test"); HtmlElementUtil.click(button); + if (d.test1Outcome!=null) throw d.test1Outcome; } - - public DescriptorImpl getDescriptor() { - return jenkins.getDescriptorByType(DescriptorImpl.class); + + @TestExtension("testValidateIsCalled") + public static final class TestValidateIsCalled implements Describable<TestValidateIsCalled>, UnprotectedRootAction { + @Override + public @CheckForNull String getIconFileName() { + return null; + } + + @Override + public @CheckForNull String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "test"; + } + + public DescriptorImpl getDescriptor() { + return Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class); + } + + @Extension + public static final class DescriptorImpl extends Descriptor<TestValidateIsCalled> { + private Exception test1Outcome; + + public void doValidateTest1(@QueryParameter("a") String a, @QueryParameter("b") boolean b, + @QueryParameter("c") boolean c, @QueryParameter("d") String d, + @QueryParameter("e") String e) { + try { + assertEquals("avalue",a); + assertTrue(b); + assertFalse(c); + assertEquals("dvalue",d); + assertEquals("e2",e); + test1Outcome = null; + } catch (Exception t) { + test1Outcome = t; + } + } + } } - - @Extension - public static final class DescriptorImpl extends Descriptor<ValidateButtonTest> { - private Exception test1Outcome; - - public void doValidateTest1(@QueryParameter("a") String a, @QueryParameter("b") boolean b, - @QueryParameter("c") boolean c, @QueryParameter("d") String d, - @QueryParameter("e") String e) { - try { - assertEquals("avalue",a); - assertTrue(b); - assertFalse(c); - assertEquals("dvalue",d); - assertEquals("e2",e); - test1Outcome = null; - } catch (Exception t) { - test1Outcome = t; + + @Test + public void noInjectionArePossible() throws Exception { + NoInjectionArePossible.DescriptorImpl d = j.jenkins.getDescriptorByType(NoInjectionArePossible.DescriptorImpl.class); + assertNotNull(d); + + checkRegularCase(d); + checkInjectionInMethod(d); + checkInjectionInWith(d); + } + + private void checkRegularCase(NoInjectionArePossible.DescriptorImpl descriptor) throws Exception { + descriptor.paramMethod = "validateInjection"; + descriptor.paramWith = "a,b"; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + descriptor.wasCalled = false; + HtmlElementUtil.click(getValidateButton(p)); + assertNotEquals("hacked", p.getTitleText()); + assertTrue(descriptor.wasCalled); + } + + private void checkInjectionInMethod(NoInjectionArePossible.DescriptorImpl descriptor) throws Exception { + descriptor.paramMethod = "validateInjection',document.title='hacked'+'"; + descriptor.paramWith = "a,b"; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + // no check on wasCalled because the button that is expected by the method is not passed (arguments are shifted due to the injection) + HtmlElementUtil.click(getValidateButton(p)); + assertNotEquals("hacked", p.getTitleText()); + } + + + private void checkInjectionInWith(NoInjectionArePossible.DescriptorImpl descriptor) throws Exception { + descriptor.paramMethod = "validateInjection"; + descriptor.paramWith = "a,b',document.title='hacked'+'"; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + descriptor.wasCalled = false; + HtmlElementUtil.click(getValidateButton(p)); + assertNotEquals("hacked", p.getTitleText()); + assertTrue(descriptor.wasCalled); + } + + private HtmlButton getValidateButton(HtmlPage page){ + DomNodeList<HtmlElement> buttons = page.getElementById("test-panel").getElementsByTagName("button"); + assertEquals(1, buttons.size()); + return (HtmlButton) buttons.get(0); + } + + @TestExtension("noInjectionArePossible") + public static final class NoInjectionArePossible implements Describable<NoInjectionArePossible>, UnprotectedRootAction { + @Override + public @CheckForNull String getIconFileName() { + return null; + } + + @Override + public @CheckForNull String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "test"; + } + + public DescriptorImpl getDescriptor() { + return Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class); + } + + @Extension + public static final class DescriptorImpl extends Descriptor<NoInjectionArePossible> { + private boolean wasCalled = false; + + public String paramMethod = "validateInjection"; + public String paramWith = null; + + public void doValidateInjection(StaplerRequest request) { + wasCalled = true; } } }
test/src/test/java/lib/layout/ConfirmationLinkTest.java+205 −0 added@@ -0,0 +1,205 @@ +/* + * The MIT License + * + * Copyright (c) 2018, 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 lib.layout; + +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.html.DomNodeList; +import com.gargoylesoftware.htmlunit.html.HtmlAnchor; +import com.gargoylesoftware.htmlunit.html.HtmlElement; +import com.gargoylesoftware.htmlunit.html.HtmlElementUtil; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.model.UnprotectedRootAction; +import hudson.util.HttpResponses; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.WebMethod; + +import javax.annotation.CheckForNull; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ConfirmationLinkTest { + @Rule + public JenkinsRule j = new JenkinsRule(); + + private static final String hrefPayload = "',document.title='hacked'+'"; + private static final String messagePayload = "',document.title='hacked'+'"; + private static final String postPayload = "document.title='hacked'"; + + @Test + public void noInjectionArePossible() throws Exception { + TestRootAction testParams = j.jenkins.getExtensionList(UnprotectedRootAction.class).get(TestRootAction.class); + assertNotNull(testParams); + + checkRegularCase(testParams); + checkRegularCasePost(testParams); + checkInjectionInHref(testParams); + checkInjectionInMessage(testParams); + checkInjectionInPost(testParams); + } + + private void checkRegularCase(TestRootAction testParams) throws Exception { + testParams.paramHref = "#"; + testParams.paramMessage = "Message to confirm the click"; + testParams.paramClass = null; + testParams.paramPost = null; + + HtmlPage p = j.createWebClient().goTo("test"); + assertTrue(p.getWebResponse().getContentAsString().contains("Message to confirm the click")); + } + + private void checkRegularCasePost(TestRootAction testParams) throws Exception { + testParams.paramHref = "submit"; + testParams.paramMessage = "Message to confirm the click"; + testParams.paramClass = null; + + testParams.paramPost = true; + assertMethodPostAfterClick(); + + testParams.paramPost = "true"; + assertMethodPostAfterClick(); + + testParams.paramPost = "TruE"; + assertMethodPostAfterClick(); + + testParams.paramPost = false; + assertMethodGetAfterClick(); + + testParams.paramPost = "false"; + assertMethodGetAfterClick(); + + testParams.paramPost = "any other string"; + assertMethodGetAfterClick(); + } + + private void assertMethodGetAfterClick() throws Exception { + Page pageAfterClick = getPageAfterClick(); + assertTrue(pageAfterClick.getWebResponse().getContentAsString().contains("method:GET")); + } + + private void assertMethodPostAfterClick() throws Exception { + Page pageAfterClick = getPageAfterClick(); + assertTrue(pageAfterClick.getWebResponse().getContentAsString().contains("method:POST")); + } + + private Page getPageAfterClick() throws Exception { + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + return HtmlElementUtil.click(getClickableLink(p)); + } + + private void checkInjectionInHref(TestRootAction testParams) throws Exception { + testParams.paramHref = hrefPayload; + testParams.paramMessage = "Message to confirm the click"; + testParams.paramClass = null; + testParams.paramPost = null; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + Page pageAfterClick = HtmlElementUtil.click(getClickableLink(p)); + assertNotEquals("hacked", p.getTitleText()); + assertTrue(p.getWebResponse().getContentAsString().contains("Message to confirm the click")); + // the url it clicks on is escaped and so does not exist + assertEquals(404, pageAfterClick.getWebResponse().getStatusCode()); + } + + private void checkInjectionInMessage(TestRootAction testParams) throws Exception { + testParams.paramHref = "#"; + testParams.paramMessage = messagePayload; + testParams.paramClass = null; + testParams.paramPost = null; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + Page pageAfterClick = HtmlElementUtil.click(getClickableLink(p)); + assertNotEquals("hacked", p.getTitleText()); + // the url is normally the same page so it's ok + assertEquals(200, pageAfterClick.getWebResponse().getStatusCode()); + } + + private void checkInjectionInPost(TestRootAction testParams) throws Exception { + testParams.paramHref = "#"; + testParams.paramMessage = "Message to confirm the click"; + testParams.paramClass = null; + testParams.paramPost = postPayload; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + Page pageAfterClick = HtmlElementUtil.click(getClickableLink(p)); + assertNotEquals("hacked", p.getTitleText()); + assertTrue(p.getWebResponse().getContentAsString().contains("Message to confirm the click")); + // the url is normally the same page so it's ok + assertEquals(200, pageAfterClick.getWebResponse().getStatusCode()); + } + + private HtmlAnchor getClickableLink(HtmlPage page){ + DomNodeList<HtmlElement> anchors = page.getElementById("test-panel").getElementsByTagName("a"); + assertEquals(1, anchors.size()); + return (HtmlAnchor) anchors.get(0); + } + + @TestExtension("noInjectionArePossible") + public static final class TestRootAction implements UnprotectedRootAction { + + public String paramHref = ""; + public String paramMessage = ""; + public String paramClass; + public Object paramPost; + + @Override + public @CheckForNull String getIconFileName() { + return null; + } + + @Override + public @CheckForNull String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "test"; + } + + @WebMethod(name = "submit") + public HttpResponse doSubmit(StaplerRequest request) { + return HttpResponses.plainText("method:" + request.getMethod()); + } + } +}
test/src/test/java/lib/layout/StopButtonTest.java+150 −0 added@@ -0,0 +1,150 @@ +/* + * The MIT License + * + * Copyright (c) 2018, 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 lib.layout; + +import com.gargoylesoftware.htmlunit.html.DomNodeList; +import com.gargoylesoftware.htmlunit.html.HtmlAnchor; +import com.gargoylesoftware.htmlunit.html.HtmlElement; +import com.gargoylesoftware.htmlunit.html.HtmlElementUtil; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.model.UnprotectedRootAction; +import hudson.util.HttpResponses; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.WebMethod; + +import javax.annotation.CheckForNull; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class StopButtonTest { + @Rule + public JenkinsRule j = new JenkinsRule(); + + private static final String hrefPayload = "\",document.title='hacked',\""; + private static final String postPayload = "\",document.title='hacked',\""; + + @Test + public void noInjectionArePossible() throws Exception { + TestRootAction testParams = j.jenkins.getExtensionList(UnprotectedRootAction.class).get(TestRootAction.class); + assertNotNull(testParams); + + checkRegularCase(testParams); + checkInjectionInHref(testParams); + checkInjectionInHrefWithConfirm(testParams); + checkInjectionInConfirm(testParams); + } + + private void checkRegularCase(TestRootAction testParams) throws Exception { + testParams.paramHref = "#"; + testParams.paramAlt = "Message to confirm the click"; + testParams.paramConfirm = null; + + HtmlPage p = j.createWebClient().goTo("test"); + assertTrue(p.getWebResponse().getContentAsString().contains("Message to confirm the click")); + } + + private void checkInjectionInHref(TestRootAction testParams) throws Exception { + testParams.paramHref = hrefPayload; + testParams.paramAlt = "Alternative text for icon"; + testParams.paramConfirm = null; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + HtmlElementUtil.click(getStopLink(p)); + assertNotEquals("hacked", p.getTitleText()); + assertTrue(p.getWebResponse().getContentAsString().contains("Alternative text for icon")); + } + + private void checkInjectionInHrefWithConfirm(TestRootAction testParams) throws Exception { + testParams.paramHref = hrefPayload; + testParams.paramAlt = "Alternative text for icon"; + testParams.paramConfirm = "Confirm message"; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + HtmlElementUtil.click(getStopLink(p)); + assertNotEquals("hacked", p.getTitleText()); + assertTrue(p.getWebResponse().getContentAsString().contains("Alternative text for icon")); + } + + private void checkInjectionInConfirm(TestRootAction testParams) throws Exception { + testParams.paramHref = "#"; + testParams.paramAlt = "Alternative text for icon"; + testParams.paramConfirm = postPayload; + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + HtmlPage p = wc.goTo("test"); + + HtmlElementUtil.click(getStopLink(p)); + assertNotEquals("hacked", p.getTitleText()); + assertTrue(p.getWebResponse().getContentAsString().contains("Alternative text for icon")); + } + + private HtmlAnchor getStopLink(HtmlPage page){ + DomNodeList<HtmlElement> anchors = page.getElementById("test-panel").getElementsByTagName("a"); + assertEquals(1, anchors.size()); + return (HtmlAnchor) anchors.get(0); + } + + @TestExtension("noInjectionArePossible") + public static class TestRootAction implements UnprotectedRootAction { + + public String paramHref = ""; + public String paramAlt = ""; + public String paramConfirm; + + @Override + public @CheckForNull String getIconFileName() { + return null; + } + + @Override + public @CheckForNull String getDisplayName() { + return null; + } + + @Override + public @CheckForNull String getUrlName() { + return "test"; + } + + @WebMethod(name = "submit") + public HttpResponse doSubmit(StaplerRequest request) { + return HttpResponses.plainText("method:" + request.getMethod()); + } + } +}
test/src/test/resources/lib/form/ExpandableTextboxTest/TestRootAction/index.jelly+33 −0 added@@ -0,0 +1,33 @@ +<!-- +The MIT License + +Copyright (c) 2018, 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. +--> +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout"> + <l:layout title="Try to inject javascript inside confirmationLink"> + <l:main-panel> + <div id="test-panel"> + <f:expandableTextbox name="${it.paramName}" /> + </div> + </l:main-panel> + </l:layout> +</j:jelly> \ No newline at end of file
test/src/test/resources/lib/form/ValidateButtonTest/NoInjectionArePossible/index.jelly+43 −0 added@@ -0,0 +1,43 @@ +<!-- +The MIT License + +Copyright (c) 2018, 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. +--> +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form"> + <l:layout title="Try to inject javascript inside validateButton"> + <l:main-panel> + <div id="test-panel"> + <f:form method="post" name="config" action="thisFormWillNotBeSubmitted"> + <!--<j:set var="instance" value="${it}" />--> + <j:set var="descriptor" value="${it.descriptor}" /> + <f:entry title="a title"> + <f:textbox name="a" value="avalue" /> + </f:entry> + <f:entry title="b title"> + <f:textbox name="b" value="bvalue" /> + </f:entry> + <f:validateButton title="test" method="${descriptor.paramMethod}" with="${descriptor.paramWith}" /> + </f:form> + </div> + </l:main-panel> + </l:layout> +</j:jelly> \ No newline at end of file
test/src/test/resources/lib/form/ValidateButtonTest/TestValidateIsCalled/index.jelly+1 −1 renamed@@ -26,7 +26,7 @@ THE SOFTWARE. Config page --> <?jelly escape-by-default='true'?> -<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> +<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form"> <l:layout title="Testing the effect of validateButton"> <l:main-panel> <f:form method="post" name="config" action="thisFormWillNotBeSubmitted">
test/src/test/resources/lib/layout/ConfirmationLinkTest/TestRootAction/index.jelly+35 −0 added@@ -0,0 +1,35 @@ +<!-- +The MIT License + +Copyright (c) 2018, 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. +--> +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout"> + <l:layout title="Try to inject javascript inside confirmationLink"> + <l:main-panel> + <div id="test-panel"> + <l:confirmationLink href="${it.paramHref}" message="${it.paramMessage}" class="${it.paramClass}" post="${it.paramPost}"> + link name + </l:confirmationLink> + </div> + </l:main-panel> + </l:layout> +</j:jelly> \ No newline at end of file
test/src/test/resources/lib/layout/StopButtonTest/TestRootAction/index.jelly+33 −0 added@@ -0,0 +1,33 @@ +<!-- +The MIT License + +Copyright (c) 2018, 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. +--> +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout"> + <l:layout title="Try to inject javascript inside stopButton"> + <l:main-panel> + <div id="test-panel"> + <l:stopButton href="${it.paramHref}" alt="${it.paramAlt}" confirm="${it.paramConfirm}" /> + </div> + </l:main-panel> + </l:layout> +</j:jelly> \ No newline at end of file
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-9jcv-v4jp-w3cqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2018-1000170ghsaADVISORY
- github.com/jenkinsci/jenkins/commit/07d18cfd6d336a2b7cc0a78e120d9739a5d4e1d1ghsaWEB
- jenkins.io/security/advisory/2018-04-11/ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.