Loading JavaScript in Jenkins Active Choices parameters
Be aware that what’s described here may introduce a security risk to your environment, and you must only do it if you really know what you are doing. You will be breaking a few security fixes of Jenkins, such as:
- SECURITY-440 https://github.com/jenkinsci/active-choices-plugin/commit/720af532b0a2c69465824af5735be2859d7c3492
- Jenkins XSS prevention https://wiki.jenkins-ci.org/JENKINS/Jelly-and-XSS-prevention.html
Some years ago, the Jenkins Active Choices plug-in had a security bug reported as a Groovy script could have malicious code that would trigger an XSS attack in Jenkins (i.e. run some arbitrary JS code in Jenkins). The plug-in got removed from the update site until we fixed it, and the solution was to sanitize the output of the Groovy script used to render the Jenkins parameters.
That broke a feature of the plug-in, where users could load third-party JavaScript files, like jQuery, D3.js, CSS Doodle, etc. This post shows how one could create a version of the plug-in that renders Jenkins parameters that load and execute any JavaScript code provided.
I tested it using a commit
from the master
branch of the plug-in:
1commit daa5837519e23377431335613661b057fe102275 (HEAD -> master, upstream/master, upstream/HEAD)
2Author: Bruno P. Kinoshita <kinow@users.noreply.github.com>
3Date: Thu Jul 31 20:38:20 2025 +0200
4
5 Update CHANGES.md
You will have to clone the repository of the plug-in, https://github.com/jenkinsci/active-choices-plugin, and then you can start by disabling escaping text in Jelly:
1$ find src/main/ -name "*.jelly" | xargs \
2 sed -i -E "s/escape-by-default='true'/escape-by-default='false'/g"
There’s one more change in DynamicReferenceParameter/formattedHtml.jelly
that you will have to apply:
1diff --git a/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/formattedHtml.jelly b/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/formattedHtml.jelly
2index a853766..c047cf1 100644
3--- a/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/formattedHtml.jelly
4+++ b/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/formattedHtml.jelly
5@@ -1,4 +1,4 @@
6-<?jelly escape-by-default='true' ?>
7+<?jelly escape-by-default='false' ?>
8 <j:jelly xmlns:j="jelly:core">
9 <j:invokeStatic var="paramName" className="org.biouno.unochoice.util.Utils" method="createRandomParameterName">
10 <j:arg type="java.lang.String" value="choice-parameter" />
11@@ -6,6 +6,6 @@
12 </j:invokeStatic>
13 <j:set var="paramName" value="${paramName}" scope="parent" />
14 <div id='formattedHtml_${paramName}'>
15- <j:out value="${it.getChoicesAsString()}"/>
16+ ${h.rawHtml(it.getChoicesAsString())}
17 </div>
18-</j:jelly>
19\ No newline at end of file
You will also need to disable escaping and sanitizing in the Java code:
1diff --git a/src/main/java/org/biouno/unochoice/AbstractCascadableParameter.java b/src/main/java/org/biouno/unochoice/AbstractCascadableParameter.java
2index 9494779..b46f126 100644
3--- a/src/main/java/org/biouno/unochoice/AbstractCascadableParameter.java
4+++ b/src/main/java/org/biouno/unochoice/AbstractCascadableParameter.java
5@@ -164,7 +164,7 @@ public abstract class AbstractCascadableParameter extends AbstractScriptablePara
6 for (String value : array) {
7 value = value.trim();
8 if (StringUtils.isNotBlank(value)) {
9- list.add(Util.escape(value));
10+ list.add(value);
11 }
12 }
13 return list.toArray(new String[0]);
14diff --git a/src/main/java/org/biouno/unochoice/AbstractScriptableParameter.java b/src/main/java/org/biouno/unochoice/AbstractScriptableParameter.java
15index 102f694..6444239 100644
16--- a/src/main/java/org/biouno/unochoice/AbstractScriptableParameter.java
17+++ b/src/main/java/org/biouno/unochoice/AbstractScriptableParameter.java
18@@ -306,7 +306,7 @@ public abstract class AbstractScriptableParameter extends AbstractUnoChoiceParam
19 String valueText = ObjectUtils.toString(entry.getValue(), "");
20 if (Utils.isSelected(valueText)) {
21 String keyText = ObjectUtils.toString(entry.getKey(), "");
22- defaultValues.add(Utils.escapeSelectedAndDisabled(keyText));
23+ defaultValues.add(keyText);
24 }
25 }
26 if (defaultValues.isEmpty()) {
27diff --git a/src/main/java/org/biouno/unochoice/model/GroovyScript.java b/src/main/java/org/biouno/unochoice/model/GroovyScript.java
28index 385aafa..c78c0a9 100644
29--- a/src/main/java/org/biouno/unochoice/model/GroovyScript.java
30+++ b/src/main/java/org/biouno/unochoice/model/GroovyScript.java
31@@ -188,9 +188,9 @@ public class GroovyScript extends AbstractScript {
32 try {
33 Object returnValue = secureScript.evaluate(cl, context, null);
34 // sanitize the text if running script in sandbox mode
35- if (secureScript.isSandbox()) {
36- returnValue = resolveTypeAndSanitize(returnValue);
37- }
38+// if (secureScript.isSandbox()) {
39+// returnValue = resolveTypeAndSanitize(returnValue);
40+// }
41 return returnValue;
42 } catch (Exception re) {
43 if (this.secureFallbackScript != null) {
44@@ -198,9 +198,9 @@ public class GroovyScript extends AbstractScript {
45 LOGGER.log(Level.FINEST, "Fallback to default script...", re);
46 Object returnValue = secureFallbackScript.evaluate(cl, context, null);
47 // sanitize the text if running script in sandbox mode
48- if (secureFallbackScript.isSandbox()) {
49- returnValue = resolveTypeAndSanitize(returnValue);
50- }
51+// if (secureFallbackScript.isSandbox()) {
52+// returnValue = resolveTypeAndSanitize(returnValue);
53+// }
54 return returnValue;
55 } catch (Exception e2) {
56 LOGGER.log(Level.WARNING, "Error executing fallback script", e2);
Now, build the project, but skip the tests since we have at least one security test that may fail with these changes.
1$ mvn clean install -DskipTests
Your modified .hpi
file should be ready to be installed in a Jenkins
server. Or you can run it locally.
1$ find . -name uno-choice.hpi
2./target/uno-choice.hpi
3$ mvn hpi:run
Finally, create your project embedding JavaScript. For instance, create a FreeStyle project with a DynamicReferenceParameter, rendering as formatted HTML.
Here’s a sample Groovy script that should render an image.
1return """
2<script type="module">
3 import 'https://esm.sh/css-doodle'
4</script>
5<css-doodle>
6 @grid: 18 / 100vmax / #0a0c27;
7 --hue: calc(180 + 1.5 * @x * @y);
8 background: hsl(var(--hue), 50%, 70%);
9 margin: -.5px;
10 transition: @r(.5s) ease;
11 clip-path: polygon(@pick(
12 '0 0, 100% 0, 100% 100%',
13 '0 0, 100% 0, 0 100%',
14 '0 0, 100% 100%, 0 100%',
15 '100% 0, 100% 100%, 0 100%'
16 ));
17</css-doodle>
18"""


Once again, this may lead to security issues if you allow users with admin permissions over jobs, or if you pipelines that load external Groovy code, and if these contain malicious code.
But if you have an internal server with code that is developed internally, and meticulously reviewed, the risk might be mitigated in your case, and you may want to be able to use JavaScript in your parameters – like we all used to be able to do before.
Someday, we may have a new permission in the plug-in, and a flag that allow admins to enable this per-project. Maybe that, combined with the Jenkins In-Process Script Approval might be enough for the plug-in to work as before, without being marked as a risk by the Jenkins CERT/security team.
(Also, as always, remember to back up and use a testbed server!)
Categories: Blog
Tags: Opensource, Java, Programming, Security