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:

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"""
Job configuration
Job configuration
Result showing a CSS Doodle
Result showing a CSS Doodle

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