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
Jenkins 2.270 through 2.393 (both inclusive), LTS 2.277.1 through 2.375.3 (both inclusive) does not escape the Jenkins version a plugin depends on when rendering the error message stating its incompatibility with the current version of Jenkins, resulting in a stored cross-site scripting (XSS) vulnerability exploitable by attackers able to provide plugins to the configured update sites and have this message shown by Jenkins instances.
The fix
[SECURITY-3037]
core/src/main/java/hudson/PluginManager.java+6 −2
@@ -1469,7 +1469,11 @@ public HttpResponse doPluginsSearch(@QueryParameter String query, @QueryParametejsonObject.put("sourceId", plugin.sourceId);jsonObject.put("title", plugin.title);jsonObject.put("displayName", plugin.getDisplayName());-jsonObject.put("wiki", plugin.wiki);+if (plugin.wiki == null || !(plugin.wiki.startsWith("https://") || plugin.wiki.startsWith("http://"))) {+jsonObject.put("wiki", StringUtils.EMPTY);+} else {+jsonObject.put("wiki", plugin.wiki);+}jsonObject.put("categories", plugin.getCategoriesStream().filter(PluginManager::isNonMetaLabel).map(UpdateCenter::getCategoryDisplayName)@@ -1486,7 +1490,7 @@ public HttpResponse doPluginsSearch(@QueryParameter String query, @QueryParametejsonObject.put("version", plugin.version);jsonObject.put("popularity", plugin.popularity);if (plugin.isForNewerHudson()) {-jsonObject.put("newerCoreRequired", Messages.PluginManager_coreWarning(plugin.requiredCore));+jsonObject.put("newerCoreRequired", Messages.PluginManager_coreWarning(Util.xmlEscape(plugin.requiredCore)));}if (plugin.hasWarnings()) {JSONObject unresolvedSecurityWarnings = new JSONObject();
test/src/test/java/hudson/PluginManagerSecurity3037Test.java+101 −0
@@ -0,0 +1,101 @@+package hudson;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertTrue;++import com.gargoylesoftware.htmlunit.AlertHandler;+import com.gargoylesoftware.htmlunit.Page;+import com.gargoylesoftware.htmlunit.html.HtmlAnchor;+import com.gargoylesoftware.htmlunit.html.HtmlElement;+import com.gargoylesoftware.htmlunit.html.HtmlElementUtil;+import com.gargoylesoftware.htmlunit.html.HtmlPage;+import hudson.model.DownloadService;+import hudson.model.RootAction;+import hudson.model.UpdateSite;+import java.io.IOException;+import java.util.ArrayList;+import java.util.Collections;+import java.util.List;+import java.util.stream.Collectors;+import javax.servlet.ServletException;+import jenkins.model.Jenkins;+import org.junit.Rule;+import org.junit.Test;+import org.jvnet.hudson.test.FlagRule;+import org.jvnet.hudson.test.JenkinsRule;+import org.jvnet.hudson.test.TestExtension;+import org.kohsuke.stapler.StaplerRequest;+import org.kohsuke.stapler.StaplerResponse;++public class PluginManagerSecurity3037Test {+@Rule+public JenkinsRule r = new JenkinsRule();++@Rule+public FlagRule<Boolean> signatureCheck = new FlagRule<>(() -> DownloadService.signatureCheck, x -> DownloadService.signatureCheck = x);++@Test+public void noInjectionOnAvailablePluginsPage() throws Exception {+DownloadService.signatureCheck = false;+Jenkins.get().getUpdateCenter().getSites().clear();+UpdateSite us = new UpdateSite("Security3037", Jenkins.get().getRootUrl() + "security3037UpdateCenter/update-center.json");+Jenkins.get().getUpdateCenter().getSites().add(us);++try (JenkinsRule.WebClient wc = r.createWebClient()) {+HtmlPage p = wc.goTo("pluginManager");+List<HtmlElement> elements = p.getElementById("bottom-sticker")+.getElementsByTagName("a")+.stream()+.filter(link -> link.getAttribute("href").equals("checkUpdatesServer"))+.collect(Collectors.toList());+assertEquals(1, elements.size());+AlertHandlerImpl alertHandler = new AlertHandlerImpl();+wc.setAlertHandler(alertHandler);++HtmlElementUtil.click(elements.get(0));+HtmlPage available = wc.goTo("pluginManager/available");+assertTrue(available.querySelector(".alert-danger")+.getTextContent().contains("This plugin is built for Jenkins 2.999"));+wc.waitForBackgroundJavaScript(100);++HtmlAnchor anchor = available.querySelector(".jenkins-table__link");+anchor.click(true, false, false);+wc.waitForBackgroundJavaScript(100);+assertTrue(alertHandler.messages.isEmpty());+}+}++static class AlertHandlerImpl implements AlertHandler {+List<String> messages = Collections.synchronizedList(new ArrayList<>());++@Override+public void handleAlert(final Page page, final String message) {+messages.add(message);+}+}++@TestExtension("noInjectionOnAvailablePluginsPage")+public static final class Security3037UpdateCenter implements RootAction {++@Override+public String getIconFileName() {+return "gear2.png";+}++@Override+public String getDisplayName() {+return "security-3037-update-center";+}++@Override+public String getUrlName() {+return "security3037UpdateCenter";+}++public void doDynamic(StaplerRequest staplerRequest, StaplerResponse staplerResponse) throws ServletException, IOException {+staplerResponse.setContentType("application/json");+staplerResponse.setStatus(200);+staplerResponse.serveFile(staplerRequest, PluginManagerSecurity3037Test.class.getResource("PluginManagerSecurity3037Test/update-center.json"));+}+}+}
test/src/test/resources/hudson/PluginManagerSecurity3037Test/update-center.json+65 −0
@@ -0,0 +1,65 @@+updateCenter.post(+{+"connectionCheckUrl": "https://www.google.com/",+"core": {+"buildDate": "Jan 31, 2023",+"name": "core",+"sha1": "TO8l3+wqx2hWtLWMHjmIOj+ERsk=",+"sha256": "BrXTCwJse5e6b/shXjv5edRJQRjxfJ/GbNrhgYcrZoU=",+"size": 98362341,+"url": "https://updates.jenkins.io/download/war/2.389/jenkins.war",+"version": "2.389"+},+"generationTimestamp": "2023-02-03T11:04:30Z",+"id": "Security3037",+"deprecations": [],+"plugins": {+"workflow-aggregator": {+"buildDate": "Jun 29, 2022",+"defaultBranch": "master",+"dependencies": [+{+"name": "pipeline-groovy-lib<img src=x onerror=alert(1)>",+"optional": false,+"version": "591.v3a_7f422b_d058<img src=x onerror=alert(1)>"+}+],+"developers": [+{+"developerId": "johndoe<img src=x onerror=alert(1)>",+"name": "john doe<img src=x onerror=alert(1)>"+}+],+"excerpt": "A suite of plugins that lets you orchestrate automation, simple or complex. See <a href=\"https://www.jenkins.io/solutions/pipeline/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Pipeline as Code with Jenkins</a> for more details.",+"gav": "org.jenkins-ci.plugins.workflow:workflow-aggregator:590.v6a_d052e5a_a_b_5<img src=x onerror=alert(1)>",+"issueTrackers": [+{+"reportUrl": "https://www.jenkins.io/participate/report-issue/redirect/#21710<img src=x onerror=alert(1)>",+"type": "jira<img src=x onerror=alert(1)>",+"viewUrl": "https://issues.jenkins.io/issues/?jql=component=21710<img src=x onerror=alert(1)>"+}+],+"labels": [+"cli<img src=x onerror=alert(1)>",+"misc",+"slaves",+"trigger"+],+"name": "workflow-aggregator<img src=x onerror=alert(1)>",+"popularity": 256809,+"previousTimestamp": "2022-05-24T12:27:03.00Z<img src=x onerror=alert(1)>",+"previousVersion": "<img src=x onerror=alert(1)>581.v0c46fa_697ffd",+"releaseTimestamp": "2022-06-29T21:02:16.00Z<img src=x onerror=alert(1)>",+"requiredCore": "2.999<img src=x onerror=alert(1)>",+"scm": "https://github.com/jenkinsci/workflow-aggregator-plugin<img src=x onerror=alert(1)>",+"sha1": "6iNlKTnXvr4W+9Svwy1+ctyqgvo=<img src=x onerror=alert(1)>",+"sha256": "P0SFgnqOBFZjxkdKORiQUbrhdlfW5YU7tOpe85bfGWE=<img src=x onerror=alert(1)>",+"size": 8282,+"title": "Pipeline<img src=x onerror=alert(1)>",+"url": "https://updates.jenkins.io/download/plugins/workflow-aggregator/590.v6a_d052e5a_a_b_5/workflow-aggregator.hpi",+"version": "590.v6a_d052e5a_a_b_5<img src=x onerror=alert(1)>",+"wiki": "javascript:alert(1)"+}+},+"updateCenterVersion": "1"+});