CVE-2020-2290
Description
Jenkins Active Choices Plugin <=2.4 has stored XSS in Reactive Reference Parameters due to unescaped script return values, exploitable by attackers with Job/Configure permission.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Active Choices Plugin <=2.4 has stored XSS in Reactive Reference Parameters due to unescaped script return values, exploitable by attackers with Job/Configure permission.
Vulnerability
Jenkins Active Choices Plugin versions 2.4 and earlier fail to properly escape return values from sandboxed Groovy scripts used by Reactive Reference Parameters, leading to a stored cross-site scripting (XSS) vulnerability [1][4]. The plugin allows scripted parameters that can generate HTML, and the missing output encoding enables injection of malicious scripts into the Jenkins interface [2].
Exploitation
An attacker with Job/Configure permission can craft a malicious script whose return value includes arbitrary HTML and JavaScript. When other users view the affected job configuration or build page, the injected script executes in their browser [1][4]. The attack does not require authentication beyond the basic permission level, and the malicious content is stored on the server, affecting all subsequent viewers [2].
Impact
Successful exploitation allows the attacker to execute arbitrary JavaScript in the context of the Jenkins web UI, potentially leading to session hijacking, credential theft, or further administrative actions on behalf of a victim user [2][4]. The vulnerability is classified as medium severity on the CVSS scale [4].
Mitigation
The plugin has been patched in version 2.6 or later [2][3]. Users should upgrade to the latest version that includes output encoding for Reactive Reference Parameters. As a workaround, administrators can restrict Job/Configure permission to trusted users only [1][2].
- GitHub - jenkinsci/active-choices-plugin: This plugin provides new scripted, dynamic parameters for freestyle jobs that can be rendered as combo-boxes, check-boxes, radio-buttons or rich HTML UI widgets.
- Jenkins Security Advisory 2020-10-08
- [SECURITY-2008] · jenkinsci/active-choices-plugin@15e3e01
- NVD - CVE-2020-2290
AI Insight generated on May 21, 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.biouno:uno-choiceMaven | < 2.5 | 2.5 |
Affected products
2- Range: unspecified
Patches
115e3e01929a6[SECURITY-2008]
3 files changed · +232 −9
src/main/java/org/biouno/unochoice/model/GroovyScript.java+64 −8 modified@@ -24,11 +24,15 @@ package org.biouno.unochoice.model; +import java.io.IOException; import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -169,21 +173,19 @@ public Object eval(Map<String, String> parameters) throws RuntimeException { try { Object returnValue = secureScript.evaluate(cl, context); - if (returnValue instanceof CharSequence) { - if (secureScript.isSandbox()) { - return SafeHtmlExtendedMarkupFormatter.INSTANCE.translate(returnValue.toString()); - } + // sanitize the text if running script in sandbox mode + if (secureScript.isSandbox()) { + returnValue = resolveTypeAndSanitize(returnValue); } return returnValue; } catch (Exception re) { if (this.secureFallbackScript != null) { try { LOGGER.log(Level.FINEST, "Fallback to default script...", re); Object returnValue = secureFallbackScript.evaluate(cl, context); - if (returnValue instanceof CharSequence) { - if (secureFallbackScript.isSandbox()) { - return SafeHtmlExtendedMarkupFormatter.INSTANCE.translate(returnValue.toString()); - } + // sanitize the text if running script in sandbox mode + if (secureFallbackScript.isSandbox()) { + returnValue = resolveTypeAndSanitize(returnValue); } return returnValue; } catch (Exception e2) { @@ -197,6 +199,60 @@ public Object eval(Map<String, String> parameters) throws RuntimeException { } } + /** + * Resolves the type of the return value, and then applies the sanitization to + * the value before returning it. + * + * <p>If the type is text, it will simply pass the value through the markup formatter.</p> + * + * <p>If the type is a list, then it will replace each value by itself sanitized + * (i.e. [sanitizeFn(value) for value in list]).</p> + * + * <p>Finally, if it is a map, does similar as with the list, and calls replaceAll to + * apply the sanitize function to each member of the map.</p> + * + * @param returnValue a value of type String, List, or Map returned after the Groovy code was evaluated + * @return sanitized value + * @throws RuntimeException if the type of the given {@code returnValue} is not String, List, or Map + */ + private Object resolveTypeAndSanitize(Object returnValue) { + if (returnValue instanceof CharSequence) { + return sanitizeString(returnValue); + } else if (returnValue instanceof List) { + List<?> list = (List<?>) returnValue; + return list.stream() + .map(r -> sanitizeString(r)) + .collect(Collectors.toList()); + } else if (returnValue instanceof Map) { + @SuppressWarnings("unchecked") + Map<Object, Object> map = (Map<Object, Object>) returnValue; + Map<Object, Object> returnMap = new HashMap<>(map.size()); + map.forEach((key, value) -> { + String newKey = sanitizeString(key); + String newValue = sanitizeString(value); + returnMap.put(newKey, newValue); + }); + return returnMap; + } + throw new RuntimeException("Return type of Groovy script must be a valid String, List, or Map"); + } + + /** + * Sanitize a string using the plug-in safe HTML markup formatter. + * @param input the input object + * @return sanitized input, or {@code null} if the input is {@code null} + */ + private String sanitizeString(Object input) { + if (input == null) { + return null; + } + try { + return SafeHtmlExtendedMarkupFormatter.INSTANCE.translate(input.toString()); + } catch (IOException e) { + throw new RuntimeException(String.format("Failed to sanitize input due to: %s", e.getMessage()), e); + } + } + /* * (non-Javadoc) *
src/main/java/org/biouno/unochoice/model/ScriptlerScript.java+1 −1 modified@@ -131,7 +131,7 @@ public GroovyScript toGroovyScript() { if (scriptler == null) { throw new RuntimeException("Missing required scriptler!"); } - return new GroovyScript(new SecureGroovyScript(scriptler.script, false, null), null); + return new GroovyScript(new SecureGroovyScript(scriptler.script, true, null), null); } // --- descriptor
src/test/java/jenkins_cert_2008/TestGroovyScriptXssVulnerabilities.java+167 −0 added@@ -0,0 +1,167 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2020 Ioannis Moutsatsos, Bruno P. Kinoshita + * + * 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 jenkins_cert_2008; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.io.IOException; +import java.util.List; + +import org.biouno.unochoice.ChoiceParameter; +import org.biouno.unochoice.DynamicReferenceParameter; +import org.biouno.unochoice.model.GroovyScript; +import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsRule.WebClient; +import org.xml.sax.SAXException; + +import com.gargoylesoftware.htmlunit.html.DomElement; +import com.gargoylesoftware.htmlunit.html.HtmlImage; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlLabel; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.HtmlTableDataCell; + +import hudson.model.FreeStyleProject; +import hudson.model.ParametersDefinitionProperty; + +/** + * Tests against XSS. See SECURITY-1954, and SECURITY-2008. + * @since 2.5 + */ +public class TestGroovyScriptXssVulnerabilities { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Tests that a {@code ChoiceParameter} using a Groovy script has its output value sanitized against XSS when + * returning a List. + * @throws IOException + * @throws SAXException + */ + @Test + public void testChoicesParameterXss() throws IOException, SAXException { + String xssString = "<img src=x onerror=alert(123)>"; + FreeStyleProject project = j.createFreeStyleProject(); + String scriptText = String.format("return ['%s']", xssString); + SecureGroovyScript secureScript = new SecureGroovyScript(scriptText, true, null); + GroovyScript script = new GroovyScript(secureScript, secureScript); + ChoiceParameter parameter = new ChoiceParameter( + xssString, + "Description", + "random-name", + script, + ChoiceParameter.PARAMETER_TYPE_CHECK_BOX, + false, + 0); + project.addProperty(new ParametersDefinitionProperty(parameter)); + project.save(); + + WebClient wc = j.createWebClient(); + wc.setThrowExceptionOnFailingStatusCode(false); + HtmlPage configPage = wc.goTo("job/" + project.getName() + "/build?delay=0sec"); + DomElement renderedParameterElement = configPage.getElementById("ecp_random-name_0"); + HtmlLabel renderedParameterLabel = (HtmlLabel) renderedParameterElement.getElementsByTagName("label").get(0); + String renderedText = renderedParameterLabel.getTextContent(); + assertNotEquals("XSS string was not escaped!", xssString, renderedText); + assertEquals("XSS string was not escaped!", "<img src=\"x\" />", renderedText); + } + + /** + * Tests that a {@code ChoiceParameter} using a Groovy script has its output value sanitized against XSS when + * returning a Map. + * @throws IOException + * @throws SAXException + */ + @Test + public void testChoicesParameterXssWithMaps() throws IOException, SAXException { + String xssString = "<img src=x onerror=alert(123)>"; + FreeStyleProject project = j.createFreeStyleProject(); + String scriptText = String.format("return ['%s': '%s']", xssString, xssString); + SecureGroovyScript secureScript = new SecureGroovyScript(scriptText, true, null); + GroovyScript script = new GroovyScript(secureScript, secureScript); + ChoiceParameter parameter = new ChoiceParameter( + xssString, + "Description", + "random-name", + script, + ChoiceParameter.PARAMETER_TYPE_RADIO, + false, + 0); + project.addProperty(new ParametersDefinitionProperty(parameter)); + project.save(); + + WebClient wc = j.createWebClient(); + wc.setThrowExceptionOnFailingStatusCode(false); + HtmlPage configPage = wc.goTo("job/" + project.getName() + "/build?delay=0sec"); + DomElement renderedParameterElement = configPage.getElementById("tbl_tr_ecp_random-name"); + HtmlInput renderedParameterInput = (HtmlInput) renderedParameterElement.getElementsByTagName("input").get(0); + String renderedText = renderedParameterInput.getAttribute("value"); + assertNotEquals("XSS string was not escaped in map key!", xssString, renderedText); + assertEquals("XSS string was not escaped in map key!", "<img src=\"x\" />", renderedText); + HtmlLabel renderedParameterLabel = (HtmlLabel) renderedParameterElement.getElementsByTagName("label").get(0); + renderedText = renderedParameterLabel.getTextContent(); + assertNotEquals("XSS string was not escaped in map key!", xssString, renderedText); + assertEquals("XSS string was not escaped in map key!", "<img src=\"x\" />", renderedText); + } + + /** + * Tests that a {@code ChoiceParameter} using a Groovy script has its output value sanitized against XSS when + * returning a String (rendered as HTML). + * @throws IOException + * @throws SAXException + */ + @Test + public void testReferenceParameterXss() throws IOException, SAXException { + String xssString = "<img src=x onerror=alert(123)>"; + FreeStyleProject project = j.createFreeStyleProject(); + String scriptText = String.format("return '%s'", xssString); + SecureGroovyScript secureScript = new SecureGroovyScript(scriptText, true, null); + GroovyScript script = new GroovyScript(secureScript, secureScript); + DynamicReferenceParameter parameter = new DynamicReferenceParameter( + xssString, + "Description", + "random-name", + script, + DynamicReferenceParameter.ELEMENT_TYPE_FORMATTED_HTML, + null, + false); + project.addProperty(new ParametersDefinitionProperty(parameter)); + project.save(); + + WebClient wc = j.createWebClient(); + wc.setThrowExceptionOnFailingStatusCode(false); + HtmlPage configPage = wc.goTo("job/" + project.getName() + "/build?delay=0sec"); + List<HtmlTableDataCell> renderedParameterElement = configPage.getByXPath("//*[@class='setting-main']"); + HtmlImage renderedParameterLabel = (HtmlImage) renderedParameterElement.get(0).getElementsByTagName("img").get(0); + String renderedText = renderedParameterLabel.asXml(); + assertNotEquals("XSS string was not escaped!", xssString, renderedText); + assertEquals("XSS string was not escaped!", "<img src=\"x\"/>", renderedText.trim()); + } + +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-rjch-j5x9-fgphghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-2290ghsaADVISORY
- www.openwall.com/lists/oss-security/2020/10/08/5ghsamailing-listx_refsource_MLISTWEB
- github.com/jenkinsci/active-choices-plugin/commit/15e3e01929a687965f44d9d06cd2d870628a54dcghsaWEB
- www.jenkins.io/security/advisory/2020-10-08/ghsax_refsource_CONFIRMWEB
News mentions
1- Jenkins Security Advisory 2020-10-08Jenkins Security Advisories · Oct 8, 2020