Cross-site Scripting vulnerability in Jenkins
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
Details
Since Jenkins 2.320 and LTS 2.332.1, help icon tooltips no longer escape the feature name, effectively undoing the fix for [SECURITY-1955](https://www.jenkins.io/security/advisory/2020-08-12/#SECURITY-1955). This vulnerability is known to be exploitable by attackers with Job/Configure permission. Jenkins 2.356, LTS 2.332.4 and LTS 2.346.1 addresses this vulnerability, the feature name in help icon tooltips is now escaped.
The fix
[SECURITY-2779]
core/src/main/resources/lib/form/helpLink.jelly+1 −1
@@ -53,7 +53,7 @@ THE SOFTWARE.</st:documentation><j:choose><j:when test="${attrs.url!=null}">-<j:set var="altText" value="${attrs.featureName != null ? '%Help for feature:' + ' ' + attrs.featureName : '%Help'}" />+<j:set var="altText" value="${attrs.featureName != null ? '%Help for feature:' + ' ' + h.escape(attrs.featureName) : '%Help'}" /><a href="#" class="jenkins-help-button" tooltip="${altText}" helpURL="${rootURL}${attrs.url}"><!-- .jenkins-help-button span element is required as it's restyled in CSS --><span>?</span>
test/src/test/java/jenkins/security/Security2779Test.java+84 −0
@@ -0,0 +1,84 @@+package jenkins.security;++import static org.hamcrest.CoreMatchers.containsString;+import static org.hamcrest.CoreMatchers.instanceOf;+import static org.hamcrest.CoreMatchers.not;+import static org.hamcrest.MatcherAssert.assertThat;++import com.gargoylesoftware.htmlunit.AlertHandler;+import com.gargoylesoftware.htmlunit.ScriptResult;+import com.gargoylesoftware.htmlunit.html.HtmlPage;+import hudson.model.UnprotectedRootAction;+import java.util.Arrays;+import java.util.Collection;+import java.util.concurrent.atomic.AtomicInteger;+import org.junit.Assert;+import org.junit.Rule;+import org.junit.Test;+import org.junit.runner.RunWith;+import org.junit.runners.Parameterized;+import org.jvnet.hudson.test.JenkinsRule;+import org.jvnet.hudson.test.TestExtension;++@RunWith(Parameterized.class)+public class Security2779Test {+public static final String URL_NAME = "security2779";+private final String selector;+@Rule+public JenkinsRule j = new JenkinsRule();++@Parameterized.Parameters+public static Collection<String> getSelectors() {+return Arrays.asList("#link-panel a", "#icon-panel svg");+}++public Security2779Test(String selector) {+this.selector = selector;+}++@Test+public void noXssInHelp() throws Exception {+final AtomicInteger alerts = new AtomicInteger();+final JenkinsRule.WebClient webClient = j.createWebClient();+webClient.setAlertHandler((AlertHandler) (p, s) -> alerts.addAndGet(1));+final HtmlPage page = webClient.goTo(URL_NAME);+final ScriptResult eventScript = page.executeJavaScript("document.querySelector('" + selector + "').dispatchEvent(new Event('mouseover'))");+final Object eventResult = eventScript.getJavaScriptResult();+assertThat(eventResult, instanceOf(boolean.class));+Assert.assertTrue((boolean) eventResult);+webClient.waitForBackgroundJavaScript(2000);+Assert.assertEquals(0, alerts.get());++final ScriptResult innerHtmlScript = page.executeJavaScript("document.querySelector('#tt').innerHTML");+Object jsResult = innerHtmlScript.getJavaScriptResult();+assertThat(jsResult, instanceOf(String.class));+String jsResultString = (String) jsResult;++// assert leading space to identify unintentional double-escaping (&lt;) as test failure+assertThat("tooltip does not contain dangerous HTML", jsResultString, not(containsString(" <img src=x")));+assertThat("tooltip contains safe text", jsResultString, containsString(" <img src=x"));+}++@TestExtension+public static class ViewHolder implements UnprotectedRootAction {++@Override+public String getIconFileName() {+return null;+}++@Override+public String getDisplayName() {+return null;+}++@Override+public String getUrlName() {+return URL_NAME;+}++public String getFeatureName() {+return " <img src=x onerror=alert(1)>";+}+}+}
test/src/test/resources/jenkins/security/Security2779Test/ViewHolder/index.jelly+13 −0
@@ -0,0 +1,13 @@+<?jelly escape-by-default='true'?>+<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form">+<l:layout title="help">+<l:main-panel>+<div id="link-panel">+<f:helpLink featureName="${it.featureName}" url="foo" />+</div>+<div id="icon-panel">+<l:helpIcon tooltip="${it.featureName}" />+</div>+</l:main-panel>+</l:layout>+</j:jelly>