Cross-site WebSocket hijacking vulnerability in the Jenkins CLI
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 has a built-in command line interface (CLI) to access Jenkins from a script or shell environment. Since Jenkins 2.217 and LTS 2.222.1, one of the ways to communicate with the CLI is through a WebSocket endpoint. This endpoint relies on the default Jenkins web request authentication functionality, like HTTP Basic authentication with API tokens, or session cookies. This endpoint is enabled when running on a version of Jetty for which Jenkins supports WebSockets. This is the case when using the provided native installers, packages, or the Docker containers, as well as when running Jenkins with the command java -jar jenkins.war. Jenkins 2.217 through 2.441 (both inclusive), LTS 2.222.1 through 2.426.2 (both inclusive) does not perform origin validation of requests made through the CLI WebSocket endpoint, resulting in a cross-site WebSocket hijacking (CSWSH) vulnerability.
The fix
[SECURITY-3315]
core/src/main/java/hudson/cli/CLIAction.java+20 −1
@@ -49,8 +49,10 @@import javax.servlet.http.HttpServletResponse;import jenkins.model.Jenkins;import jenkins.util.FullDuplexHttpService;+import jenkins.util.SystemProperties;import jenkins.websocket.WebSocketSession;import jenkins.websocket.WebSockets;+import org.apache.commons.lang.StringUtils;import org.jenkinsci.Symbol;import org.kohsuke.accmod.Restricted;import org.kohsuke.accmod.restrictions.NoExternalUse;@@ -73,6 +75,12 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy {private static final Logger LOGGER = Logger.getLogger(CLIAction.class.getName());+/**+* Boolean values map to allowing/disallowing WS CLI endpoint always, {@code null} is the default of doing an {@code Origin} check.+* {@code true} is only advisable if anonymous users have no permissions, and Jenkins sends SameSite=Lax cookies (or browsers use that as the implicit default).+*/+/* package-private for testing */ static /* non-final for Script Console */ Boolean ALLOW_WEBSOCKET = SystemProperties.optBoolean(CLIAction.class.getName() + ".ALLOW_WEBSOCKET");+private final transient Map<UUID, FullDuplexHttpService> duplexServices = new HashMap<>();@Override@@ -114,10 +122,21 @@ public boolean isWebSocketSupported() {/*** WebSocket endpoint.*/-public HttpResponse doWs() {+public HttpResponse doWs(StaplerRequest req) {if (!WebSockets.isSupported()) {return HttpResponses.notFound();}+if (ALLOW_WEBSOCKET == null) {+final String actualOrigin = req.getHeader("Origin");+final String expectedOrigin = StringUtils.removeEnd(StringUtils.removeEnd(Jenkins.get().getRootUrlFromRequest(), "/"), req.getContextPath());++if (actualOrigin == null || !actualOrigin.equals(expectedOrigin)) {+LOGGER.log(Level.FINE, () -> "Rejecting origin: " + actualOrigin + "; expected was from request: " + expectedOrigin);+return HttpResponses.forbidden();+}+} else if (!ALLOW_WEBSOCKET) {+return HttpResponses.forbidden();+}Authentication authentication = Jenkins.getAuthentication2();return WebSockets.upgrade(new WebSocketSession() {ServerSideImpl connection;
test/src/test/java/hudson/cli/Security3315Test.java+62 −0
@@ -0,0 +1,62 @@+package hudson.cli;++import static org.hamcrest.CoreMatchers.is;+import static org.hamcrest.MatcherAssert.assertThat;++import java.io.IOException;+import java.net.URL;+import java.util.Arrays;+import java.util.List;+import org.htmlunit.HttpMethod;+import org.htmlunit.Page;+import org.htmlunit.WebRequest;+import org.junit.Rule;+import org.junit.Test;+import org.junit.runner.RunWith;+import org.junit.runners.Parameterized;+import org.jvnet.hudson.test.FlagRule;+import org.jvnet.hudson.test.JenkinsRule;++@RunWith(Parameterized.class)+public class Security3315Test {+@Rule+public JenkinsRule j = new JenkinsRule();++@Rule+public FlagRule<Boolean> escapeHatch;++private final Boolean allowWs;++@Parameterized.Parameters+public static List<String> escapeHatchValues() {+return Arrays.asList(null, "true", "false");+}++public Security3315Test(String allowWs) {+this.allowWs = allowWs == null ? null : Boolean.valueOf(allowWs);+this.escapeHatch = new FlagRule<>(() -> CLIAction.ALLOW_WEBSOCKET, v -> { CLIAction.ALLOW_WEBSOCKET = v; }, this.allowWs);+}++@Test+public void test() throws IOException {+try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {+// HTTP 400 is WebSocket "success" (HTMLUnit doesn't support it)+final URL jenkinsUrl = j.getURL();+WebRequest request = new WebRequest(new URL(jenkinsUrl.toString() + "cli/ws"), HttpMethod.GET);+Page page = wc.getPage(request);+assertThat(page.getWebResponse().getStatusCode(), is(allowWs == Boolean.TRUE ? 400 : 403)); // no Origin header++request.setAdditionalHeader("Origin", jenkinsUrl.getProtocol() + "://example.org:" + jenkinsUrl.getPort());+page = wc.getPage(request);+assertThat(page.getWebResponse().getStatusCode(), is(allowWs == Boolean.TRUE ? 400 : 403)); // Wrong Origin host++request.setAdditionalHeader("Origin", jenkinsUrl.getProtocol() + "://" + jenkinsUrl.getHost());+page = wc.getPage(request);+assertThat(page.getWebResponse().getStatusCode(), is(allowWs == Boolean.TRUE ? 400 : 403)); // Wrong Origin port++request.setAdditionalHeader("Origin", jenkinsUrl.getProtocol() + "://" + jenkinsUrl.getHost() + ":" + jenkinsUrl.getPort());+page = wc.getPage(request);+assertThat(page.getWebResponse().getStatusCode(), is(allowWs == Boolean.FALSE ? 403 : 400)); // Reject correct Origin if ALLOW_WS is explicitly false+}+}+}
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2024-23898
- WEBhttps://github.com/jenkinsci/jenkins/commit/de450967f38398169650b55c002f1229a3fcdb1b
- PACKAGEhttps://github.com/jenkinsci/jenkins
- WEBhttps://www.jenkins.io/security/advisory/2024-01-24/#SECURITY-3315
- WEBhttps://www.sonarsource.com/blog/excessive-expansion-uncovering-critical-security-vulnerabilities-in-jenkins
- WEBhttp://www.openwall.com/lists/oss-security/2024/01/24/6