Security context
High· 8.0GHSA-62wf-24c4-8r76 CVE-2022-34170CWE-22CWE-79Published Jun 24, 2022

Cross-site Scripting vulnerability in Jenkins

Research this vulnerability

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

2.350 → fixed in 2.3562.320 → fixed in 2.332.42.346 → fixed in 2.346.1

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]

Daniel Beck· Jun 9, 2022, 03:46 AM+981f71495a63f
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 (&amp;lt;) as test failure
+ assertThat("tooltip does not contain dangerous HTML", jsResultString, not(containsString(" <img src=x")));
+ assertThat("tooltip contains safe text", jsResultString, containsString(" &lt;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>

References