Preventing Cross-Site Scripting in Jelly views

Cross-Site Scripting (XSS) is a web application vulnerability that allows users with the ability to control what gets shown to other users on a web page to run scripts in their browser.

Jenkins displays content written by users with different levels of access in many different areas, and the Jelly files typically contain placeholders like this:

<h1>Project ${it.name}</h1>

If it.name evaluates to a string containing HTML tags, those will be placed verbatim into the output, for example:

<h1>Project <script src="https://evil.com/exploit-jenkins.js"></script>my java build</h1>
In old versions of Jenkins, use of the <st:out> tag allowed escaping specific expressions. This document discusses the much safer escape by default approach (below).

Escaping by Default

Since Jenkins 1.339, it is possible to instruct the Jelly processor to escape ${...} expressions like the above by default. To do that, place the following XML processing instruction in the first line of the Jelly document:

<?jelly escape-by-default='true'?>
<!-- rest of the document here -->

Note that this only affects the use of ${...} among PCDATA, and not in attribute values, so that Jelly tag invocations don’t result in surprising behavior.

Keep Your Your Toolchain Updated

Since plugin POM 1.596, Jelly files are required to escape variables by default, and the build fails if such problems are found. Therefore it is strongly recommended to use at least that version of the plugin parent POM to prevent accidental XSS vulnerabilities.

It’s recommended to always use the newest available 2.x or newer plugin parent POM. Learn more.

Escaping and Localized Expressions

Localized expressions of the form ${%expression} are also affected by the above in the following ways:

  1. The localized expression (read from a resource file) is not escaped, and allows inline HTML.

  2. Any arguments to the expression are escaped.

This means the following works as expected (rendering the a tag), and still prevents XSS problems in the user-specified name:

index.jelly
<?jelly escape-by-default='true'?>
<p>${%blurb(it.displayName)}</p>
index.properties
blurb=<a href="https://jenkins.io/">{0}</a>

Rendering Pre-Escaped Content

In rare cases, it might be necessary to render content that has previously been escaped or otherwise processed, without further escaping. A common reason to do this is to support formatting using the markup formatter configured for the current Jenkins instance, with might support a safe subset of HTML, or other markup languages, like Markdown. In those cases, the security is controlled by the markup formatter.

To do that, use the <j:out> Jelly tag:

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
  <j:out value="${app.markupFormatter.translate(it.description)}"/>
</j:jelly>

To pass arguments to localized expressions without escaping them in escaped-by-default Jelly files, wrap them in a call to Functions#rawHtml:

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
  <h1>${%welcomeMessage(h.rawHtml(it.markupFormattedName)}</h1>
</j:jelly>
If the source of the argument to j:out or Functions#rawHtml isn’t completely trusted, these might result in an XSS vulnerability.

Passing values to JavaScript

The bad way

In a situation where you need to pass information to JavaScript, you might inject the value directly inside the script like in the following snippet.

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
  <h1>Random title</h1>
  <div id="target-div"></div>

  <script>
     var textToInsert = "${variableFromJava}";
     document.querySelector('#target-div').innerText = textToInsert;
  </script>
</j:jelly>

In this case, if the variableFromJava comes from an unsafe source, it could be used to create a cross-site scripting (XSS) attack. For example the following code will be executed: ";alert(123);". The JavaScript sent to the client will be:

var textToInsert = "";alert(123);"";
Within the payload, we close the existing double quote, then execute our code and finally open a double quote to match the existing one. That will result in a script without any format error.

The good way

If you need to pass a variable to your JavaScript, we strongly recommend to use this approach instead:

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
  <h1>Random title</h1>
  <div id="target-div" data-inserted-from-java="${variableFromJava}"></div>

  <script>
     var targetDiv = document.querySelector('#target-div');
     var textToInsert = targetDiv.getAttribute('data-inserted-from-java');
     // equivalent of: targetDiv.dataset.insertedFromJava
     targetDiv.innerText = textToInsert;
  </script>
</j:jelly>

The use of the data-* attributes allow the developer either to use getAttribute or the dataset property, for more information see the MDN documentation.

In this case you are taking advantage of the Jelly escape mechanism to ensure that the result is just a String. If we try to inject a ", it will be automatically converted to &quot;.

In the previous (vulnerable) example, the injected code was inside a context of "code to be interpreted".