Security context
Medium· 5.4GHSA-wv63-gwr9-5c55 CVE-2021-21608CWE-79Published May 24, 2022

Stored XSS vulnerability in Jenkins button labels

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.2752.263.2 → fixed in 2.275

Details

Jenkins 2.274 and earlier, LTS 2.263.1 and earlier does not escape button labels in the Jenkins UI. This results in a cross-site scripting vulnerability exploitable by attackers with the ability to control button labels. An example of buttons with a user-controlled label are the buttons of the Pipeline `input` step. Jenkins 2.275, LTS 2.263.2 escapes button labels in the Jenkins UI.

The fix

[SECURITY-2035][SECURITY-2171]

Matt Sicker· Jan 11, 2021, 04:52 PM+585248c451b0888
core/src/main/resources/hudson/tools/ToolInstallation/global.jelly+1 0
@@ -26,6 +26,7 @@ THE SOFTWARE.
<f:section title="${descriptor.displayName}">
<d:taglib uri="local">
<d:tag name="section">
+ <j:set var="escapeEntryTitleAndDescription" value="true"/>
<f:entry title="${%title(descriptor.displayName)}" description="${%description(descriptor.displayName)}">
<f:repeatable name="tool" var="instance" items="${descriptor.installations}"
add="${%label.add(descriptor.displayName)}" header="${descriptor.displayName}" enableTopButton="true"
core/src/main/resources/lib/form/hetero-list/hetero-list.js+6 1
@@ -26,7 +26,12 @@ Behaviour.specify("DIV.hetero-list-container", 'hetero-list', -100, function(e)
var name = n.getAttribute("name");
var tooltip = n.getAttribute("tooltip");
var descriptorId = n.getAttribute("descriptorId");
- menu.options[i] = new Option(n.getAttribute("title"),""+i);
+ // YUI Menu interprets this <option> text node as HTML, so let's escape it again!
+ var title = n.getAttribute("title");
+ if (title) {
+ title = title.escapeHTML();
+ }
+ menu.options[i] = new Option(title,""+i);
templates.push({html:n.innerHTML, name:name, tooltip:tooltip,descriptorId:descriptorId});
i++;
});
test/src/test/java/hudson/model/ViewSEC2171Test.java+215 0
@@ -0,0 +1,215 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts
+ *
+ * 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.model;
+
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.TestExtension;
+
+import javax.annotation.CheckForNull;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+//TODO to be merged back into ViewTest after the security release
+public class ViewSEC2171Test {
+
+ @Rule public JenkinsRule j = new JenkinsRule();
+
+ @Test
+ @Issue("SECURITY-2171")
+ public void newJob_xssPreventedInId() throws Exception {
+ CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);
+ customizableTLID.customId = "regularclass\" onclick=alert(123) other=\"";
+ customizableTLID.customDisplayName = "DN-xss-id";
+
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("view/all/newJob");
+
+ Object result = page.executeJavaScript("Array.from(document.querySelectorAll('.label')).filter(el => el.innerText.indexOf('" + customizableTLID.customDisplayName + "') !== -1)[0].parentElement.parentElement").getJavaScriptResult();
+ assertThat(result, instanceOf(HTMLElement.class));
+ HTMLElement resultElement = (HTMLElement) result;
+ assertThat(resultElement.getAttribute("onclick", null), nullValue());
+ }
+
+ @Test
+ @Issue("SECURITY-2171")
+ public void newJob_xssPreventedInDisplayName() throws Exception {
+ CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);
+ customizableTLID.customId = "xss-dn";
+ customizableTLID.customDisplayName = "DN <img src=x onerror=console.warn(123)>";
+
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("view/all/newJob");
+
+ Object result = page.executeJavaScript("document.querySelector('.xss-dn .label').innerHTML").getJavaScriptResult();
+ assertThat(result, instanceOf(String.class));
+ String resultString = (String) result;
+ assertThat(resultString, not(containsString("<")));
+ }
+
+ @Test
+ public void newJob_descriptionSupportsHtml() throws Exception {
+ CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);
+ customizableTLID.customId = "html-desc";
+ customizableTLID.customDescription = "Super <strong>looong</strong> description";
+
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("view/all/newJob");
+
+ Object result = page.executeJavaScript("document.querySelector('.html-desc .desc strong')").getJavaScriptResult();
+ assertThat(result, instanceOf(HTMLElement.class));
+ assertThat(((HTMLElement) result).getTagName(), is("STRONG"));
+ }
+
+ @Test
+ @Issue("SECURITY-2171")
+ public void newJob_xssPreventedInGetIconFilePathPattern() throws Exception {
+ CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);
+ customizableTLID.customId = "xss-ifpp";
+ customizableTLID.customIconClassName = null;
+ customizableTLID.customIconFilePathPattern = "\"><img src=x onerror=\"alert(123)";
+
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("view/all/newJob");
+
+ Object resultIconChildrenCount = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .icon').children.length").getJavaScriptResult();
+ assertThat(resultIconChildrenCount, instanceOf(Integer.class));
+ int resultIconChildrenCountInt = (int) resultIconChildrenCount;
+ assertEquals(1, resultIconChildrenCountInt);
+
+ Object resultImgAttributesCount = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .icon img').attributes.length").getJavaScriptResult();
+ assertThat(resultImgAttributesCount, instanceOf(Integer.class));
+ int resultImgAttributesCountInt = (int) resultImgAttributesCount;
+ assertEquals(1, resultImgAttributesCountInt);
+ }
+
+ @Test
+ public void newJob_iconClassName() throws Exception {
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("view/all/newJob");
+
+ Object resultClassNames = page.executeJavaScript("document.querySelector('.hudson_model_FreeStyleProject .icon img').className").getJavaScriptResult();
+ assertThat(resultClassNames, instanceOf(String.class));
+ String resultClassNamesString = (String) resultClassNames;
+ List<String> resultClassNamesList = Arrays.asList(resultClassNamesString.split(" "));
+ assertThat(resultClassNamesList, hasItem("icon-xlg"));
+ assertThat(resultClassNamesList, hasItem("icon-freestyle-project"));
+
+ Object resultSrc = page.executeJavaScript("document.querySelector('.hudson_model_FreeStyleProject .icon img').src").getJavaScriptResult();
+ assertThat(resultSrc, instanceOf(String.class));
+ String resultSrcString = (String) resultSrc;
+ assertThat(resultSrcString, containsString("48x48"));
+ assertThat(resultSrcString, containsString("freestyleproject.png"));
+ }
+
+ @Test
+ public void newJob_twoLetterIcon() throws Exception {
+ CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);
+ customizableTLID.customId = "two-letters-desc";
+ customizableTLID.customDisplayName = "Two words";
+ customizableTLID.customIconClassName = null;
+ customizableTLID.customIconFilePathPattern = null;
+
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("view/all/newJob");
+
+ Object result = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .default-icon')").getJavaScriptResult();
+ assertThat(result, instanceOf(HTMLElement.class));
+ HTMLElement resultHtml = (HTMLElement) result;
+ HTMLElement spanA = (HTMLElement) resultHtml.getFirstElementChild();
+ HTMLElement spanB = (HTMLElement) resultHtml.getLastElementChild();
+ assertThat(spanA.getClassName_js(), is("a"));
+ assertThat(spanA.getInnerText(), is("T"));
+ assertThat(spanB.getClassName_js(), is("b"));
+ assertThat(spanB.getInnerText(), is("w"));
+ }
+
+ @TestExtension
+ public static class CustomizableTLID extends TopLevelItemDescriptor {
+
+ public String customId = "ID-not-yet-defined";
+ public String customDisplayName = "DisplayName-not-yet-defined";
+ public String customDescription = "Description-not-yet-defined";
+ public String customIconFilePathPattern = "IconFilePathPattern-not-yet-defined";
+ public String customIconClassName = "IconClassName-not-yet-defined";
+
+ public CustomizableTLID() {
+ super(FreeStyleProject.class);
+ }
+
+ @Override
+ public String getId() {
+ return customId;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return customDisplayName;
+ }
+
+ @Override
+ public String getDescription() {
+ return customDescription;
+ }
+
+ @Override
+ public @CheckForNull String getIconFilePathPattern() {
+ return customIconFilePathPattern;
+ }
+
+ @Override
+ public String getIconClassName() {
+ return customIconClassName;
+ }
+
+ @Override
+ public TopLevelItem newInstance(ItemGroup parent, String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+}
test/src/test/java/lib/form/HeteroListSEC2035Test.java+283 0
@@ -0,0 +1,283 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2020, 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.form;
+
+import com.gargoylesoftware.htmlunit.html.DomElement;
+import com.gargoylesoftware.htmlunit.html.HtmlButton;
+import com.gargoylesoftware.htmlunit.html.HtmlElementUtil;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLAnchorElement;
+import hudson.ExtensionList;
+import hudson.model.Describable;
+import hudson.model.Descriptor;
+import hudson.model.UnprotectedRootAction;
+import hudson.tools.ToolDescriptor;
+import hudson.tools.ToolInstallation;
+import hudson.tools.ToolInstaller;
+import hudson.tools.ToolProperty;
+import hudson.util.FormValidation;
+import net.sourceforge.htmlunit.corejs.javascript.NativeArray;
+import org.jenkinsci.Symbol;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.TestExtension;
+
+import javax.annotation.CheckForNull;
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+//TODO rename to HeteroListTest after security release
+public class HeteroListSEC2035Test {
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Test
+ @Issue("SECURITY-2035")
+ public void xssPrevented_heteroList_usingDescriptorDisplayName() throws Exception {
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ RootActionImpl rootAction = ExtensionList.lookupSingleton(RootActionImpl.class);
+ TestItemDescribable.DynamicDisplayNameDescriptor dynamic = ExtensionList.lookupSingleton(TestItemDescribable.DynamicDisplayNameDescriptor.class);
+ rootAction.descriptorList = Arrays.asList(dynamic);
+
+ dynamic.displayName = "Display<strong>Name</strong>";
+
+ HtmlPage page = wc.goTo("root");
+
+ page.executeJavaScript("document.querySelector('.hetero-list-add').click();");
+ Object result = page.executeJavaScript("document.querySelector('.yuimenuitem a')").getJavaScriptResult();
+ assertThat(result, instanceOf(HTMLAnchorElement.class));
+ HTMLAnchorElement menuItem = (HTMLAnchorElement) result;
+ String menuItemContent = menuItem.getInnerHTML();
+ assertThat(menuItemContent, not(containsString("<")));
+ }
+
+ // correspond to the hardening of escapeEntryTitleAndDescription
+ @Test
+ @Issue("SECURITY-2035")
+ public void xssPrevented_usingToolInstallation_withJustDisplayName() throws Exception {
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("configureTools/");
+
+ // check the displayName
+ Object resultDN = page.executeJavaScript(
+ "var settingFields = document.querySelectorAll('.setting-name');" +
+ "var children = Array.from(settingFields).filter(b => b.textContent.indexOf('XSS:') !== -1)[0].children;" +
+ "Array.from(children).filter(c => c.tagName === 'IMG')"
+ ).getJavaScriptResult();
+ assertThat(resultDN, instanceOf(NativeArray.class));
+ NativeArray resultDNNA = (NativeArray) resultDN;
+ assertEquals(0, resultDNNA.size());
+
+ // check the description
+ Object resultDesc = page.executeJavaScript(
+ "var settingFields = document.querySelectorAll('.setting-description');" +
+ "var children = Array.from(settingFields).filter(b => b.textContent.indexOf('XSS:') !== -1)[0].children;" +
+ "Array.from(children).filter(c => c.tagName === 'IMG')"
+ ).getJavaScriptResult();
+ assertThat(resultDesc, instanceOf(NativeArray.class));
+ NativeArray resultDescNA = (NativeArray) resultDesc;
+ assertEquals(0, resultDescNA.size());
+ }
+
+ @Test
+ @Issue("SECURITY-2035")
+ public void xssPrevented_usingToolInstallation_repeatableAddExisting() throws Exception {
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("configureTools/");
+
+ // the existing add button can already trigger an XSS
+ Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('Add XSS') !== -1)[0].innerHTML").getJavaScriptResult();
+ assertThat(result, instanceOf(String.class));
+ String resultString = (String) result;
+ assertThat(resultString, not(containsString("<")));
+ }
+
+ // only possible after a partial fix
+ @Test
+ public void xssPrevented_usingToolInstallation_repeatableAddAfterClick() throws Exception {
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("configureTools/");
+
+ Optional<DomElement> addXssButtonRawOptional = page.getElementsByTagName("button").stream().filter(e -> e.getTextContent().contains("Add XSS")).findFirst();
+ assertTrue(addXssButtonRawOptional.isPresent());
+ assertThat(addXssButtonRawOptional.get(), instanceOf(HtmlButton.class));
+
+ HtmlButton addXssButton = (HtmlButton) addXssButtonRawOptional.get();
+ HtmlElementUtil.click(addXssButton);
+
+ // checking only the newly created button (at the top of the panel), hence the [0]
+ Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('Add XSS') !== -1)[0].innerHTML").getJavaScriptResult();
+ assertThat(result, instanceOf(String.class));
+ String resultString = (String) result;
+ assertThat(resultString, not(containsString("<")));
+ }
+
+ @Test
+ @Issue("SECURITY-2035")
+ public void xssPrevented_usingToolInstallation_repeatableAddWithExistingUsingInstallationsButton() throws Exception {
+ Xss.DescriptorImpl xssDescriptor = ExtensionList.lookupSingleton(Xss.DescriptorImpl.class);
+ xssDescriptor.installations = new Xss[]{ new Xss("name1", "home1", null) };
+
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("configureTools/");
+
+ // XSS: [img] installations...
+ Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('XSS:') !== -1)[0].innerHTML").getJavaScriptResult();
+ assertThat(result, instanceOf(String.class));
+ String resultString = (String) result;
+ assertThat(resultString, not(containsString("<")));
+ }
+
+ @Test
+ @Issue("SECURITY-2035")
+ public void xssPrevented_usingToolInstallation_repeatableAddWithExistingAfterOpening() throws Exception {
+ Xss.DescriptorImpl xssDescriptor = ExtensionList.lookupSingleton(Xss.DescriptorImpl.class);
+ xssDescriptor.installations = new Xss[]{ new Xss("name1", "home1", null) };
+
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("configureTools/");
+
+ // Passing the installation button
+ page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('XSS:') !== -1)[0].click()");
+
+ // Looking for all the buttons displayed, at this point there is one Add, one Delete and the second Add.
+ // Both add are generated through different code.
+ // While keeping away the installations... advanced button as it's covered in its own test
+ Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('XSS') !== -1 && b.textContent.indexOf('...') === -1).map(b => b.innerHTML)").getJavaScriptResult();
+ assertThat(result, instanceOf(List.class));
+ List resultArray = (List) result;
+ for (int i = 0; i < resultArray.size(); i++) {
+ assertThat((String) resultArray.get(i), not(containsString("<")));
+ }
+
+ // "delete" then "add" makes us coming back in scenario covered by xssUsingToolInstallationRepeatableAdd
+ }
+
+ @Test
+ @Issue("SECURITY-2035")
+ public void xssPrevented_usingToolInstallation_repeatableDelete() throws Exception {
+ JenkinsRule.WebClient wc = j.createWebClient();
+
+ HtmlPage page = wc.goTo("configureTools/");
+
+ // we could also re-use the same method as used in xssUsingToolInstallationRepeatableAdd
+ page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('Add XSS') !== -1)[0].click()");
+
+ Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('Delete XSS') !== -1)[0].innerHTML").getJavaScriptResult();
+ assertThat(result, instanceOf(String.class));
+ String resultString = (String) result;
+ assertThat(resultString, not(containsString("<")));
+ }
+
+ public static class TestItemDescribable implements Describable<TestItemDescribable> {
+ public Descriptor<TestItemDescribable> getDescriptor() {
+ return ExtensionList.lookupSingleton(DynamicDisplayNameDescriptor.class);
+ }
+
+ @TestExtension
+ public static class DynamicDisplayNameDescriptor extends Descriptor<TestItemDescribable> {
+ public String displayName = "NotYetDefined";
+
+ @Override
+ public String getDisplayName() {
+ return displayName;
+ }
+ }
+ }
+
+ @TestExtension
+ public static class RootActionImpl implements UnprotectedRootAction {
+ public List<Descriptor<?>> descriptorList;
+
+ @CheckForNull
+ public String getIconFileName() {
+ return null;
+ }
+
+ @CheckForNull
+ public String getDisplayName() {
+ return null;
+ }
+
+ @CheckForNull
+ public String getUrlName() {
+ return "root";
+ }
+ }
+
+ public static final class Xss extends ToolInstallation {
+
+ public Xss(String name, String home, List<? extends ToolProperty<?>> properties) {
+ super(name, home, properties);
+ }
+
+ @TestExtension
+ @Symbol("tool-xss")
+ public static class DescriptorImpl extends ToolDescriptor<Xss> {
+ private Xss[] installations = new Xss[0];
+
+ public String getDisplayName() {
+ return "XSS: <img src=x onerror=console.warn('" + getClass().getName() +"') />";
+ }
+
+ public @Override Xss[] getInstallations() {
+ return installations;
+ }
+
+ public @Override void setInstallations(Xss... xsses) {
+ this.installations = xsses;
+ }
+
+ @Override
+ public List<? extends ToolInstaller> getDefaultInstallers() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Checks if the JAVA_HOME is a valid JAVA_HOME path.
+ */
+ @Override protected FormValidation checkHomeDirectory(File value) {
+ return FormValidation.ok();
+ }
+ }
+ }
+}
test/src/test/resources/lib/form/HeteroListSEC2035Test/RootActionImpl/index.jelly+8 0
@@ -0,0 +1,8 @@
+<?jelly escape-by-default='true'?>
+<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:hetero-list name="no-care" descriptors="${it.descriptorList}"/>
+ </l:main-panel>
+</l:layout>
+</j:jelly>
war/src/main/js/add-item.js+63 22
@@ -160,9 +160,30 @@ $.when(getItems()).done(function(data) {
}
function drawItem(elem) {
- var desc = checkForLink(elem.description);
- var $item = $(['<li tabindex="0" role="radio" aria-checked="false" class="', cleanClassName(elem.class), '"><label><input type="radio" name="mode" value="',
- elem.class ,'"/> <span class="label">', elem.displayName, '</span></label></li>'].join('')).append(['<div class="desc">', desc, '</div>'].join('')).append(drawIcon(elem));
+ var item = document.createElement('li');
+ item.tabIndex = 0;
+ item.className = cleanClassName(elem.class);
+ item.setAttribute('role', 'radio');
+ item.setAttribute('aria-checked', 'false');
+
+ var label = item.appendChild(document.createElement('label'));
+
+ var radio = label.appendChild(document.createElement('input'));
+ radio.type = 'radio';
+ radio.name = 'mode';
+ radio.value = elem.class;
+
+ var displayName = label.appendChild(document.createElement('span'));
+ displayName.className = 'label';
+
+ displayName.appendChild(document.createTextNode(elem.displayName));
+
+ var desc = item.appendChild(document.createElement('div'));
+ desc.className = 'desc';
+ desc.innerHTML = checkForLink(elem.description);
+
+ var iconDiv = drawIcon(elem);
+ item.appendChild(iconDiv);
function select(e) {
e.preventDefault();
@@ -183,32 +204,41 @@ $.when(getItems()).done(function(data) {
}
}
- $item.click(select);
-
- $item.keypress(function(e) {
- switch (e.which) {
- case 13:
- case 32:
- $(this).trigger('click');
- e.stopPropagation();
- break;
+ item.addEventListener('click', select);
+ item.addEventListener('keydown', function (evt) {
+ if (evt.code === 'Space' || evt.code === 'Enter') {
+ this.click();
+ evt.stopPropagation();
}
});
- return $item;
+ return item;
}
function drawIcon(elem) {
- var $icn;
+ var iconDiv = document.createElement('div');
if (elem.iconClassName && elem.iconQualifiedUrl) {
- $icn = $('<div class="icon">');
- $(['<img class="', elem.iconClassName, ' icon-xlg" src="', elem.iconQualifiedUrl, '">'].join('')).appendTo($icn);
+ iconDiv.className = 'icon';
+
+ var img1 = document.createElement('img');
+ img1.className = elem.iconClassName + ' icon-xlg';
+ img1.src = elem.iconQualifiedUrl;
+ iconDiv.appendChild(img1);
+
+ // Example for Freestyle project
+ // <div class="icon"><img class="icon-freestyle-project icon-xlg" src="/jenkins/static/108b2346/images/48x48/freestyleproject.png"></div>
} else if (elem.iconFilePathPattern) {
- $icn = $('<div class="icon">');
+ iconDiv.className = 'icon';
+
var iconFilePath = jRoot + '/' + elem.iconFilePathPattern.replace(":size", "48x48");
- $(['<img src="', iconFilePath, '">'].join('')).appendTo($icn);
+
+ var img2 = document.createElement('img');
+ img2.src = iconFilePath;
+ iconDiv.appendChild(img2);
+
+ // Example for Maven project
+ // <div class="icon"><img src="/jenkins/plugin/maven-plugin/images/48x48/mavenmoduleset.png"></div>
} else {
- $icn = $('<div class="default-icon">');
var colors = ['c-49728B','c-335061','c-D33833','c-6D6B6D', 'c-6699CC'];
var desc = elem.description || '';
var name = elem.displayName;
@@ -216,10 +246,21 @@ $.when(getItems()).done(function(data) {
var aName = name.split(' ');
var a = name.substring(0,1);
var b = ((aName.length === 1) ? name.substring(1,2) : aName[1].substring(0,1));
- $(['<span class="a">',a,'</span><span class="b">',b,'</span>'].join('')).appendTo($icn);
- $icn.addClass(colorClass);
+
+ var spanFakeImgA = document.createElement('span');
+ spanFakeImgA.className = "a";
+ spanFakeImgA.innerText = a;
+ iconDiv.appendChild(spanFakeImgA);
+ var spanFakeImgB = document.createElement('span');
+ spanFakeImgB.className = "b";
+ spanFakeImgB.innerText = b;
+ iconDiv.appendChild(spanFakeImgB);
+ iconDiv.className = colorClass + ' default-icon';
+
+ // Example for MockFolder
+ // <div class="default-icon c-49728B"><span class="a">M</span><span class="b">o</span></div>
}
- return $icn;
+ return iconDiv;
}
// The main panel content is hidden by default via an inline style. We're ready to remove that now.
war/src/main/webapp/scripts/hudson-behavior.js+9 1
@@ -611,7 +611,15 @@ function makeButton(e,onclick) {
var h = e.onclick;
var clsName = e.className;
var n = e.name;
- var btn = new YAHOO.widget.Button(e,{});
+
+ var attributes = {};
+ // YUI Button class interprets value attribute of <input> as HTML
+ // similar to how the child nodes of a <button> are treated as HTML.
+ // in standard HTML, we wouldn't expect the former case, yet here we are!
+ if (e.tagName === 'INPUT') {
+ attributes.label = e.value.escapeHTML();
+ }
+ var btn = new YAHOO.widget.Button(e, attributes);
if(onclick!=null)
btn.addListener("click",onclick);
if(h!=null)

References