From e9725bd22880e5f6f3e4cbb11b031b7e260be171 Mon Sep 17 00:00:00 2001 From: Paul King Date: Tue, 30 Jun 2026 21:54:18 +1000 Subject: [PATCH] GROOVY-12119: JmxBuilder: connector environment map is discarded --- .../builder/JmxServerConnectorFactory.groovy | 24 +++++- .../JmxServerConnectorFactoryTest.groovy | 78 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/subprojects/groovy-jmx/src/main/groovy/groovy/jmx/builder/JmxServerConnectorFactory.groovy b/subprojects/groovy-jmx/src/main/groovy/groovy/jmx/builder/JmxServerConnectorFactory.groovy index 54b32c4e9bb..e53a6306d02 100644 --- a/subprojects/groovy-jmx/src/main/groovy/groovy/jmx/builder/JmxServerConnectorFactory.groovy +++ b/subprojects/groovy-jmx/src/main/groovy/groovy/jmx/builder/JmxServerConnectorFactory.groovy @@ -23,6 +23,7 @@ import javax.management.remote.JMXConnectorServer import javax.management.remote.JMXConnectorServerFactory import javax.management.remote.JMXServiceURL import javax.management.remote.rmi.RMIConnectorServer +import javax.net.ssl.SSLContext import javax.rmi.ssl.SslRMIClientSocketFactory import javax.rmi.ssl.SslRMIServerSocketFactory @@ -53,6 +54,23 @@ class JmxServerConnectorFactory extends AbstractFactory { private static final List SUPPORTED_PROTOCOLS = ["rmi", "jrmp", "jmxmp"] + // Restrict the SSL server socket factory to modern TLS versions rather than relying on + // the JVM default protocol set (which may still enable TLS 1.0/1.1 on older/misconfigured JREs). + // Intersect with the protocols the running JDK actually supports so we never request an + // unsupported protocol (which the factory would reject), e.g. TLS 1.3 on a JDK without it. + private static final String[] ENABLED_TLS_PROTOCOLS = pinnedTlsProtocols() + + private static String[] pinnedTlsProtocols() { + Set preferred = ['TLSv1.3', 'TLSv1.2'] + try { + Set supported = SSLContext.getDefault().supportedSSLParameters.protocols + Set usable = preferred.intersect(supported) + return (usable ?: preferred) as String[] + } catch (Exception ignored) { + return preferred as String[] + } + } + /** * Creates a server connector for the supplied connection settings. * @@ -140,14 +158,14 @@ class JmxServerConnectorFactory extends AbstractFactory { env.put("com.sun.management.jmxremote.access.file", aFile) // SSL connection - def ssl = props.remove("com.sun.management.jmxremote. ssl") ?: props.remove("sslEnabled") + def ssl = props.remove("com.sun.management.jmxremote.ssl") ?: props.remove("sslEnabled") env.put("com.sun.management.jmxremote.ssl", ssl) // config other rmi props if (protocol == "rmi") { if (ssl) { def csf = props.remove(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE) ?: new SslRMIClientSocketFactory() - def ssf = props.remove(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE) ?: new SslRMIServerSocketFactory() + def ssf = props.remove(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE) ?: new SslRMIServerSocketFactory((String[]) null, ENABLED_TLS_PROTOCOLS, false) env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf) env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, ssf) } @@ -158,6 +176,8 @@ class JmxServerConnectorFactory extends AbstractFactory { } props.clear() + + return env } private JMXServiceURL generateServiceUrl(def protocol, def host, def port) { diff --git a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxServerConnectorFactoryTest.groovy b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxServerConnectorFactoryTest.groovy index dd7cbe588a3..9271dda07d3 100644 --- a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxServerConnectorFactoryTest.groovy +++ b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxServerConnectorFactoryTest.groovy @@ -27,6 +27,9 @@ import javax.management.remote.JMXConnector import javax.management.remote.JMXConnectorFactory import javax.management.remote.JMXServiceURL import javax.management.remote.rmi.RMIConnectorServer +import javax.net.ssl.SSLContext +import javax.rmi.ssl.SslRMIClientSocketFactory +import javax.rmi.ssl.SslRMIServerSocketFactory @ExtendWith(CgroupV2NpeMitigationExtension) class JmxServerConnectorFactoryTest { @@ -70,4 +73,79 @@ class JmxServerConnectorFactoryTest { result.stop() } + // GROOVY-12119: connector properties were silently discarded because the property-building + // method ended in props.clear() (a void call) and so implicitly returned null instead of the env map. + @Test + void testConnectorPropertiesAreReturned_Groovy12119() { + def factory = new JmxServerConnectorFactory() + def env = factory.confiConnectorProperties('rmi', rmi.port, [authenticate: false]) + + assert env != null : 'connector environment map must not be discarded' + // supplied/derived entries are present + assert env.containsKey('com.sun.management.jmxremote.authenticate') + } + + // GROOVY-12119: when SSL is requested the env map must carry the SSL socket factories + @Test + void testConnectorPropertiesApplySsl_Groovy12119() { + def factory = new JmxServerConnectorFactory() + def env = factory.confiConnectorProperties('rmi', rmi.port, [sslEnabled: true]) + + assert env != null + assert env['com.sun.management.jmxremote.ssl'] + assert env[RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE] instanceof SslRMIServerSocketFactory + assert env[RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE] instanceof SslRMIClientSocketFactory + } + + // GROOVY-12119: without SSL no socket factories should be added (but env is still returned) + @Test + void testConnectorPropertiesWithoutSsl_Groovy12119() { + def factory = new JmxServerConnectorFactory() + def env = factory.confiConnectorProperties('rmi', rmi.port, [authenticate: false]) + + assert env != null + assert !env.containsKey(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE) + assert !env.containsKey(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE) + } + + // GROOVY-12119: an SSL-enabled connector is built and started without error (end-to-end smoke test) + @Test + void testJmxServerConnectorWithSsl_Groovy12119() { + RMIConnectorServer result = builder.serverConnector(port: rmi.port, properties: [sslEnabled: true]) + + assert result + result.start() + assert result.isActive() + result.stop() + } + + // GROOVY-12119: the canonical 'com.sun.management.jmxremote.ssl' property key is recognised + // (previously the key literal contained a stray space so the standard key never matched) + @Test + void testConnectorRecognizesCanonicalSslKey_Groovy12119() { + def factory = new JmxServerConnectorFactory() + def env = factory.confiConnectorProperties('rmi', rmi.port, ['com.sun.management.jmxremote.ssl': true]) + + assert env != null + assert env['com.sun.management.jmxremote.ssl'] + assert env[RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE] instanceof SslRMIServerSocketFactory + } + + // GROOVY-12119: the SSL server socket factory is pinned to modern TLS rather than the JVM default set, + // but only to protocols the running JDK actually supports (so old JDKs without TLS 1.3 are not locked out) + @Test + void testSslServerSocketFactoryRestrictsProtocols_Groovy12119() { + def factory = new JmxServerConnectorFactory() + def env = factory.confiConnectorProperties('rmi', rmi.port, [sslEnabled: true]) + + SslRMIServerSocketFactory ssf = (SslRMIServerSocketFactory) env[RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE] + def enabled = ssf.enabledProtocols as Set + def supported = SSLContext.getDefault().supportedSSLParameters.protocols as Set + + assert !enabled.isEmpty() + assert enabled.every { it in supported } // never requests an unsupported protocol -> no lockout + assert enabled.every { it in ['TLSv1.3', 'TLSv1.2'] } // modern TLS only, no legacy 1.0/1.1 + assert 'TLSv1.2' in enabled // always present on JDK 8+ + } + }