VYPR
High severityOSV Advisory· Published Dec 10, 2025· Updated Dec 16, 2025

CVE-2025-67635

CVE-2025-67635

Description

Jenkins 2.540 and earlier, LTS 2.528.2 and earlier does not properly close HTTP-based CLI connections when the connection stream becomes corrupted, allowing unauthenticated attackers to cause a denial of service.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.jenkins-ci.main:jenkins-coreMaven
>= 2.529, < 2.5412.541
org.jenkins-ci.main:cliMaven
>= 2.529, < 2.5412.541
org.jenkins-ci.main:jenkins-coreMaven
< 2.528.32.528.3
org.jenkins-ci.main:cliMaven
< 2.528.32.528.3

Affected products

1

Patches

1
efa181632202

[SECURITY-3630]

https://github.com/jenkinsci/jenkinsDaniel BeckDec 2, 2025via ghsa
4 files changed · +154 8
  • cli/src/main/java/hudson/cli/PlainCLIProtocol.java+1 2 modified
    @@ -153,15 +153,14 @@ public void run() {
                     }
                 } catch (ClosedChannelException x) {
                     LOGGER.log(Level.FINE, null, x);
    -                side.handleClose();
                 } catch (IOException x) {
                     LOGGER.log(Level.WARNING, null, flightRecorder.analyzeCrash(x, "broken stream"));
                 } catch (ReadPendingException x) {
                     // in case trick in CLIAction does not work
                     LOGGER.log(Level.FINE, null, x);
    -                side.handleClose();
                 } catch (RuntimeException x) {
                     LOGGER.log(Level.WARNING, null, x);
    +            } finally {
                     side.handleClose();
                 }
             }
    
  • core/src/main/java/hudson/cli/CLIAction.java+9 4 modified
    @@ -40,11 +40,11 @@
     import java.nio.charset.Charset;
     import java.nio.charset.UnsupportedCharsetException;
     import java.util.ArrayList;
    -import java.util.HashMap;
     import java.util.List;
     import java.util.Locale;
     import java.util.Map;
     import java.util.UUID;
    +import java.util.concurrent.ConcurrentHashMap;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import jenkins.model.Jenkins;
    @@ -80,7 +80,7 @@ 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");
     
    -    private final transient Map<UUID, FullDuplexHttpService> duplexServices = new HashMap<>();
    +    private final transient Map<UUID, FullDuplexHttpService> duplexServices = new ConcurrentHashMap<>();
     
         @Override
         public String getIconFileName() {
    @@ -315,8 +315,13 @@ private synchronized void ready() {
     
             void run() throws IOException, InterruptedException {
                 synchronized (this) {
    -                while (!ready) {
    -                    wait();
    +                long end = System.currentTimeMillis() + FullDuplexHttpService.CONNECTION_TIMEOUT;
    +                while (!ready && System.currentTimeMillis() < end) {
    +                    wait(1000);
    +                }
    +                if (!ready) {
    +                    LOGGER.log(Level.FINE, "CLI timeout waiting for client");
    +                    return;
                     }
                 }
                 PrintStream stdout = new PrintStream(streamStdout(), false, encoding);
    
  • core/src/main/java/jenkins/util/FullDuplexHttpService.java+9 2 modified
    @@ -142,8 +142,15 @@ public synchronized void upload(StaplerRequest2 req, StaplerResponse2 rsp) throw
             notify();
     
             // wait until we are done
    -        while (!completed) {
    -            wait();
    +        long end = System.currentTimeMillis() + CONNECTION_TIMEOUT;
    +        while (!completed && System.currentTimeMillis() < end) {
    +            LOGGER.log(Level.FINE, "Waiting for upload stream {0} for {1}: {2}", new Object[] {upload, uuid, this});
    +            wait(1000);
    +        }
    +
    +        if (!completed) {
    +            LOGGER.log(Level.FINE, "Timeout reached for {0} for {1}: {2}", new Object[] {upload, uuid, this});
    +            throw new IOException("CLI timeout");
             }
         }
     
    
  • test/src/test/java/hudson/cli/Security3630Test.java+135 0 added
    @@ -0,0 +1,135 @@
    +package hudson.cli;
    +
    +import static org.hamcrest.MatcherAssert.assertThat;
    +import static org.hamcrest.Matchers.allOf;
    +import static org.hamcrest.Matchers.containsString;
    +import static org.hamcrest.Matchers.empty;
    +import static org.hamcrest.Matchers.hasItem;
    +import static org.hamcrest.Matchers.hasProperty;
    +import static org.hamcrest.Matchers.instanceOf;
    +import static org.hamcrest.Matchers.not;
    +import static org.hamcrest.Matchers.nullValue;
    +import static org.jvnet.hudson.test.LoggerRule.recorded;
    +
    +import hudson.Functions;
    +import hudson.init.impl.InstallUncaughtExceptionHandler;
    +import java.io.File;
    +import java.io.IOException;
    +import java.lang.management.ThreadInfo;
    +import java.net.URL;
    +import java.util.ArrayList;
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.Set;
    +import java.util.UUID;
    +import java.util.concurrent.Callable;
    +import java.util.concurrent.ExecutionException;
    +import java.util.concurrent.ExecutorService;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.Future;
    +import java.util.concurrent.TimeUnit;
    +import java.util.logging.Level;
    +import java.util.stream.Collectors;
    +import jenkins.util.FullDuplexHttpService;
    +import org.apache.commons.io.FileUtils;
    +import org.awaitility.Awaitility;
    +import org.htmlunit.HttpMethod;
    +import org.htmlunit.WebRequest;
    +import org.junit.jupiter.api.AfterEach;
    +import org.junit.jupiter.api.BeforeEach;
    +import org.junit.jupiter.api.Test;
    +import org.junit.jupiter.api.io.TempDir;
    +import org.junit.platform.commons.util.ExceptionUtils;
    +import org.jvnet.hudson.test.JenkinsRule;
    +import org.jvnet.hudson.test.LoggerRule;
    +import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
    +
    +@WithJenkins
    +public class Security3630Test {
    +    public static final int CONCURRENCY = 50;
    +    public static final int ITERATIONS = 50;
    +    private JenkinsRule j = new JenkinsRule();
    +
    +    @TempDir
    +    private File tmp;
    +
    +    private LoggerRule loggerRule = new LoggerRule().record(InstallUncaughtExceptionHandler.class.getName(), Level.WARNING);
    +
    +    private long originalTimeout;
    +
    +    @BeforeEach
    +    public void setup(JenkinsRule j) throws Exception {
    +        this.j = j;
    +        // TODO FlagRule in JUnit 5?
    +        originalTimeout = FullDuplexHttpService.CONNECTION_TIMEOUT;
    +        FullDuplexHttpService.CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(5);
    +    }
    +
    +    @AfterEach
    +    public void reset() {
    +        FullDuplexHttpService.CONNECTION_TIMEOUT = originalTimeout;
    +    }
    +
    +    @Test
    +    void control() throws IOException {
    +        loggerRule.capture(100);
    +        final String uuid = UUID.randomUUID().toString();
    +        try (JenkinsRule.WebClient wc = j.createWebClient()) {
    +            WebRequest request = new WebRequest(new URL(j.getURL().toString() + "cli?remoting=false"));
    +            request.setHttpMethod(HttpMethod.POST);
    +            request.setAdditionalHeader("Session", uuid);
    +            request.setAdditionalHeader("Side", "download");
    +            wc.getPage(request);
    +        }
    +        assertThat(loggerRule, recorded(nullValue(String.class), allOf(instanceOf(IOException.class), hasProperty("message", containsString("HTTP full-duplex channel timeout: " + uuid)))));
    +    }
    +
    +    @Test
    +    void testHashMap() throws InterruptedException, IOException {
    +        // If this test appears flaky, it's probably not: The race condition cannot be reliably triggered.
    +        // If the assertion fails, then there's probably a bug here.
    +        // TODO Do we want to keep a test like this?
    +        loggerRule.capture(100);
    +        final File jar = File.createTempFile("jenkins-cli.jar", null, tmp);
    +        FileUtils.copyURLToFile(j.jenkins.getJnlpJars("jenkins-cli.jar").getURL(), jar);
    +        for (int i = 0; i < ITERATIONS; i++) {
    +            List<Callable<Void>> callables = new ArrayList<>();
    +            for (int c = 0; c < CONCURRENCY; c++) {
    +                callables.add(() -> {
    +                    new ProcessBuilder().command("java", "-jar", jar.getAbsolutePath(), "-http", "-s", j.getURL().toString(), "who-am-i").start().waitFor();
    +                    return null;
    +                });
    +            }
    +
    +            ExecutorService executor = Executors.newFixedThreadPool(CONCURRENCY);
    +            List<Future<Void>> futures = executor.invokeAll(callables);
    +
    +            futures.forEach(f -> {
    +                try {
    +                    f.get();
    +                } catch (InterruptedException | ExecutionException e) {
    +                    throw new RuntimeException(e);
    +                }
    +            });
    +            assertThat(loggerRule.getRecords().stream().map(r -> String.join("\n", r.getMessage(), r.getLoggerName(), r.getThrown().getMessage(), ExceptionUtils.readStackTrace(r.getThrown()))).collect(Collectors.toList()), empty());
    +            // See #control() assertion for the "expected" failure without the fix.
    +        }
    +    }
    +
    +    @Test
    +    void testIndefiniteWait() throws IOException {
    +        var stream = new hudson.cli.FullDuplexHttpStream(j.getURL(), "cli?remoting=false", null);
    +        {
    +            Set<String> threadNames = Arrays.stream(Functions.getThreadInfos()).map(ThreadInfo::getThreadName).collect(Collectors.toSet());
    +            assertThat(threadNames, hasItem(containsString("Handling POST /jenkins/cli")));
    +        }
    +        stream.getOutputStream().write(new byte[10]);
    +        stream.getOutputStream().close();
    +        stream.getInputStream().close();
    +
    +        Awaitility.await().atMost(FullDuplexHttpService.CONNECTION_TIMEOUT * 2, TimeUnit.MILLISECONDS).untilAsserted(() -> {
    +            Set<String> threadNames = Arrays.stream(Functions.getThreadInfos()).map(ThreadInfo::getThreadName).collect(Collectors.toSet());
    +            assertThat(threadNames, not(hasItem(containsString("Handling POST /jenkins/cli"))));
    +        });
    +    }
    +}
    

Vulnerability mechanics

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

References

5

News mentions

0

No linked articles in our index yet.