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
In Jenkins 2.196 and earlier, LTS 2.176.3 and earlier, the f:expandableTextBox form control interpreted its content as HTML when expanded, resulting in a stored XSS vulnerability exploitable by users with permission to define its contents (typically Job/Configure).
The fix
[SECURITY-1498]
test/src/test/java/lib/form/ExpandableTextboxSEC1498Test.java+58 −0
@@ -0,0 +1,58 @@+package lib.form;++import com.gargoylesoftware.htmlunit.html.HtmlElementUtil;+import com.gargoylesoftware.htmlunit.html.HtmlInput;+import com.gargoylesoftware.htmlunit.html.HtmlPage;+import hudson.model.FreeStyleProject;+import hudson.model.Job;+import jenkins.model.OptionalJobProperty;+import org.junit.Rule;+import org.junit.Test;+import org.jvnet.hudson.test.JenkinsRule;+import org.jvnet.hudson.test.TestExtension;++import static org.junit.Assert.assertEquals;++//TODO meant to be merged back into ExpandableTextboxTest after security release to avoid conflict during the upmerge process+public class ExpandableTextboxSEC1498Test {+@Rule+public JenkinsRule j = new JenkinsRule();++@Test+public void noXssUsingInputValue() throws Exception {+XssProperty xssProperty = new XssProperty("</textarea><h1>HACK</h1>");+FreeStyleProject p = j.createFreeStyleProject();+p.addProperty(xssProperty);++JenkinsRule.WebClient wc = j.createWebClient();+HtmlPage configurePage = wc.getPage(p, "configure");++int numberOfH1Before = configurePage.getElementsByTagName("h1").size();++HtmlInput xssInput = configurePage.getElementByName("_.xss");+HtmlInput expandButton = (HtmlInput) xssInput.getParentNode().getNextSibling().getFirstChild();+HtmlElementUtil.click(expandButton);++// no additional h1, meaning the "payload" is not interpreted+int numberOfH1After = configurePage.getElementsByTagName("h1").size();++assertEquals(numberOfH1Before, numberOfH1After);+}++public static final class XssProperty extends OptionalJobProperty<Job<?,?>> {++private String xss;++public XssProperty(String xss){+this.xss = xss;+}++public String getXss() {+return xss;+}++@TestExtension("noXssUsingInputValue")+public static class DescriptorImpl extends OptionalJobProperty.OptionalJobPropertyDescriptor {+}+}+}
test/src/test/resources/lib/form/ExpandableTextboxSEC1498Test/XssProperty/config-details.jelly+29 −0
@@ -0,0 +1,29 @@+<!--+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.+-->+<?jelly escape-by-default='true'?>+<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">+<f:entry field="xss">+<f:expandableTextbox />+</f:entry>+</j:jelly>
war/src/main/webapp/scripts/hudson-behavior.js+6 −2
@@ -1505,8 +1505,12 @@ function expandTextArea(button,id) {n = n.parentNode;}-n.parentNode.innerHTML =-"<textarea rows=8 class='setting-input' name='"+field.name+"'>"+value+"</textarea>";+var parent = n.parentNode;+parent.innerHTML = "<textarea rows=8 class='setting-input'></textarea>";+var textArea = parent.childNodes[0];+textArea.name = field.name;+textArea.innerText = value;+layoutUpdateCallback.call();}