VYPR
High severityNVD Advisory· Published Mar 18, 2026· Updated Mar 19, 2026

CVE-2026-33002

CVE-2026-33002

Description

Jenkins 2.442 through 2.554 (both inclusive), LTS 2.426.3 through LTS 2.541.2 (both inclusive) performs origin validation of requests made through the CLI WebSocket endpoint by computing the expected origin for comparison using the Host or X-Forwarded-Host HTTP request headers, making it vulnerable to DNS rebinding attacks that allow bypassing origin validation.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Jenkins uses the Host or X-Forwarded-Host header to validate WebSocket CLI origins, enabling DNS rebinding attacks that bypass origin checks.

Vulnerability

Overview

CVE-2026-33002 is a high-severity vulnerability in Jenkins core Jenkins, affecting versions 2.442 through 2.554 (inclusive) and LTS 2.426.3 through LTS 2.541.2 (inclusive). The Jenkins CLI WebSocket endpoint validates the Origin header by computing the expected origin using the Host or X-Forwarded-Host HTTP request headers [1][2]. This design flaw allows an attacker to manipulate the expected origin through DNS rebinding, effectively bypassing the origin validation mechanism.

Exploitation and

Attack Surface

An attacker can exploit this vulnerability by performing a DNS rebinding attack. The attacker controls a domain that initially resolves to a benign IP address, passing the origin check, then quickly switches to resolve to the victim's Jenkins server IP. Because Jenkins computes the expected origin from the Host header (which the attacker can control via DNS rebinding), the malicious WebSocket request appears to come from an allowed origin [2][3]. The WebSocket endpoint is enabled by default on supported Jetty versions and relies on standard Jenkins authentication (e.g., HTTP Basic with API tokens or session cookies) [2]. No special network position is required beyond the ability to perform DNS rebinding.

Impact

Successful exploitation allows an attacker to bypass origin validation for the CLI WebSocket endpoint. This can lead to unauthorized access can lead to arbitrary command execution on the Jenkins controller, as the CLI provides full administrative capabilities if the attacker can authenticate or hijack a session. The vulnerability is rated High (CVSS 4.0 score not yet provided by NVD) [1][2].

Mitigation

Jenkins has released fixed versions: Jenkins 2.555 (weekly) and LTS 2.541.3. The fix changes origin validation to use the Jenkins configured URL (from JenkinsLocationConfiguration) rather than the Host header, the Host header [3]. Users should upgrade immediately. No workaround is available; the WebSocket endpoint cannot be disabled without affecting CLI functionality [2].

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.jenkins-ci.main:jenkins-coreMaven
>= 2.442, < 2.5552.555

Affected products

2

Patches

1
348666da7136

[SECURITY-3674]

https://github.com/jenkinsci/jenkinsDaniel BeckMar 10, 2026via ghsa
2 files changed · +83 2
  • core/src/main/java/hudson/cli/CLIAction.java+16 2 modified
    @@ -48,6 +48,7 @@
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import jenkins.model.Jenkins;
    +import jenkins.model.JenkinsLocationConfiguration;
     import jenkins.util.FullDuplexHttpService;
     import jenkins.util.SystemProperties;
     import jenkins.websocket.WebSocketSession;
    @@ -80,6 +81,12 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy {
          */
         /* package-private for testing */ static /* non-final for Script Console */ Boolean ALLOW_WEBSOCKET = SystemProperties.optBoolean(CLIAction.class.getName() + ".ALLOW_WEBSOCKET");
     
    +    /**
    +     * If this is set to {@code true}, {@link Jenkins#getRootUrlFromRequest()} is used to validate the {@code Origin} header.
    +     * This can be a security issue if Jenkins is running on a local network without authentication as it allows DNS rebinding attacks.
    +     */
    +    /* package-private for testing */ static /* non-final for Script Console */ boolean ACCEPT_URL_FROM_REQUEST = SystemProperties.getBoolean(CLIAction.class.getName() + ".ACCEPT_URL_FROM_REQUEST");
    +
         private final transient Map<UUID, FullDuplexHttpService> duplexServices = new ConcurrentHashMap<>();
     
         @Override
    @@ -142,7 +149,14 @@ public HttpResponse doWs(StaplerRequest2 req) {
             if (ALLOW_WEBSOCKET == null) {
                 final String actualOrigin = req.getHeader("Origin");
     
    -            String o = Jenkins.get().getRootUrlFromRequest();
    +            // If Jenkins URL is not configured, Jenkins#getRootUrl falls back to Jenkins#getRootUrlFromRequest, so look this up directly
    +            String o = JenkinsLocationConfiguration.get().getUrl();
    +            if (ACCEPT_URL_FROM_REQUEST) {
    +                o = Jenkins.get().getRootUrlFromRequest();
    +            }
    +            if (o == null) {
    +                return statusWithExplanation(HttpServletResponse.SC_FORBIDDEN, "Jenkins URL is not configured (set Jenkins URL in the configuration)");
    +            }
                 String removeSuffix1 = "/";
                 if (o.endsWith(removeSuffix1)) {
                     o = o.substring(0, o.length() - removeSuffix1.length());
    @@ -154,7 +168,7 @@ public HttpResponse doWs(StaplerRequest2 req) {
                 final String expectedOrigin = o;
     
                 if (actualOrigin == null || !actualOrigin.equals(expectedOrigin)) {
    -                LOGGER.log(Level.FINE, () -> "Rejecting origin: " + actualOrigin + "; expected was from request: " + expectedOrigin);
    +                LOGGER.log(Level.FINE, () -> "Rejecting origin: " + actualOrigin + "; expected was: " + expectedOrigin);
                     return statusWithExplanation(HttpServletResponse.SC_FORBIDDEN, "Unexpected request origin (check your reverse proxy settings)");
                 }
             } else if (!ALLOW_WEBSOCKET) {
    
  • test/src/test/java/hudson/cli/Security3315Test.java+67 0 modified
    @@ -7,12 +7,15 @@
     import java.net.URL;
     import java.util.Arrays;
     import java.util.List;
    +import jenkins.model.JenkinsLocationConfiguration;
     import org.htmlunit.HttpMethod;
     import org.htmlunit.Page;
     import org.htmlunit.WebRequest;
     import org.junit.jupiter.api.BeforeEach;
    +import org.junit.jupiter.api.Test;
     import org.junit.jupiter.params.ParameterizedTest;
     import org.junit.jupiter.params.provider.MethodSource;
    +import org.jvnet.hudson.test.Issue;
     import org.jvnet.hudson.test.JenkinsRule;
     import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
     
    @@ -53,6 +56,70 @@ void test(Boolean allowWs) throws IOException {
                 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
    +        } finally {
    +            CLIAction.ALLOW_WEBSOCKET = null;
    +        }
    +    }
    +
    +    @Issue("SECURITY-3674")
    +    @Test
    +    void testDifferentDomain() throws IOException {
    +        final URL jenkinsUrl = j.getURL();
    +        // safety assertions
    +        final String jenkinsUrlHost = "localhost";
    +        assertThat(jenkinsUrl.getHost(), is(jenkinsUrlHost));
    +        assertThat(JenkinsLocationConfiguration.get().getUrl(), is(jenkinsUrl.toString()));
    +
    +        try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
    +            {
    +                // control: Regular request to the configured domain
    +                final WebRequest request = new WebRequest(new URL(jenkinsUrl.toString() + "cli/ws"), HttpMethod.GET);
    +                // correct Origin
    +                request.setAdditionalHeader("Origin", jenkinsUrl.getProtocol() + "://" + jenkinsUrl.getHost() + ":" + jenkinsUrl.getPort());
    +                final Page page = wc.getPage(request);
    +                assertThat(page.getWebResponse().getStatusCode(), is(400)); // 400 is WebSocket "success" (HTMLUnit doesn't support it)
    +            }
    +
    +            final String alternativeUrlHost = "127.0.0.1";
    +            final URL alternativeUrl = new URL(jenkinsUrl.toString().replace(jenkinsUrlHost, alternativeUrlHost));
    +            {
    +                // wrong but matching request domain and origin, prohibited by default
    +                final WebRequest request = new WebRequest(new URL(alternativeUrl.toString() + "cli/ws"), HttpMethod.GET);
    +                request.setAdditionalHeader("Origin", jenkinsUrl.getProtocol() + "://" + alternativeUrlHost + ":" + jenkinsUrl.getPort());
    +                final Page page = wc.getPage(request);
    +                assertThat(page.getWebResponse().getStatusCode(), is(403));
    +            }
    +
    +            CLIAction.ACCEPT_URL_FROM_REQUEST = true;
    +            try {
    +                // wrong but matching request domain and origin, allowed by escape hatch
    +                final WebRequest request = new WebRequest(new URL(alternativeUrl.toString() + "cli/ws"), HttpMethod.GET);
    +                request.setAdditionalHeader("Origin", jenkinsUrl.getProtocol() + "://" + alternativeUrlHost + ":" + jenkinsUrl.getPort());
    +                final Page page = wc.getPage(request);
    +                assertThat(page.getWebResponse().getStatusCode(), is(400));
    +            } finally {
    +                CLIAction.ACCEPT_URL_FROM_REQUEST = false;
    +            }
    +
    +            JenkinsLocationConfiguration.get().setUrl(null);
    +            {
    +                // correct request domain and origin, but no configured URL, prohibited
    +                final WebRequest request = new WebRequest(new URL(jenkinsUrl.toString() + "cli/ws"), HttpMethod.GET);
    +                request.setAdditionalHeader("Origin", jenkinsUrl.getProtocol() + "://" + jenkinsUrl.getHost() + ":" + jenkinsUrl.getPort());
    +                final Page page = wc.getPage(request);
    +                assertThat(page.getWebResponse().getStatusCode(), is(403));
    +            }
    +
    +            CLIAction.ACCEPT_URL_FROM_REQUEST = true;
    +            try {
    +                // wrong but matching request domain and origin, allowed by escape hatch even without URL
    +                final WebRequest request = new WebRequest(new URL(alternativeUrl.toString() + "cli/ws"), HttpMethod.GET);
    +                request.setAdditionalHeader("Origin", jenkinsUrl.getProtocol() + "://" + alternativeUrlHost + ":" + jenkinsUrl.getPort());
    +                final Page page = wc.getPage(request);
    +                assertThat(page.getWebResponse().getStatusCode(), is(400));
    +            } finally {
    +                CLIAction.ACCEPT_URL_FROM_REQUEST = false;
    +            }
             }
         }
     }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

1