This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch release/2.25.3
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/release/2.25.3 by this push:
new 5350d1054c Fix host name verification in `SSLSocketManager` (#4002)
5350d1054c is described below
commit 5350d1054cd85d8089c90fb2c107fcf1e1c935c5
Author: Volkan Yazıcı <[email protected]>
AuthorDate: Mon Dec 15 13:02:19 2025 +0100
Fix host name verification in `SSLSocketManager` (#4002)
---
.../log4j/core/net/SslSocketManagerTest.java | 131 +++++++++++++++++++++
.../log4j/core/net/ssl/SslSocketManagerTest.java | 45 -------
.../logging/log4j/core/net/SslSocketManager.java | 98 ++++++++++++---
.../log4j/core/net/ssl/SslConfiguration.java | 12 +-
src/changelog/2.25.3/.release-notes.adoc.ftl | 2 +
.../4002_fix_SslSocketAppender_verifyHostName.xml | 12 ++
.../ROOT/pages/manual/appenders/network.adoc | 3 +
7 files changed, 240 insertions(+), 63 deletions(-)
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/SslSocketManagerTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/SslSocketManagerTest.java
new file mode 100644
index 0000000000..07ef8bd0fe
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/SslSocketManagerTest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.net;
+
+import static
org.apache.logging.log4j.core.net.SslSocketManager.createSniHostName;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
+import org.apache.logging.log4j.core.net.ssl.SslKeyStoreConstants;
+import org.apache.logging.log4j.core.net.ssl.TrustStoreConfiguration;
+import org.apache.logging.log4j.test.junit.UsingStatusListener;
+import org.apache.logging.log4j.util.Strings;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junitpioneer.jupiter.Issue;
+
+class SslSocketManagerTest {
+
+ private static final String HOST_NAME = "apache.org";
+
+ private static final int HOST_PORT = 443;
+
+ private static final Layout<?> LAYOUT =
PatternLayout.createDefaultLayout();
+
+ @Test
+ @Issue("https://github.com/apache/logging-log4j2/issues/3947")
+ @UsingStatusListener // Suppresses `StatusLogger` output, unless there is
a failure
+ void should_not_throw_exception_when_configuration_without_KeyStore()
throws Exception {
+ final TrustStoreConfiguration trustStoreConfig = new
TrustStoreConfiguration(
+ SslKeyStoreConstants.TRUSTSTORE_LOCATION,
+ SslKeyStoreConstants::TRUSTSTORE_PWD,
+ SslKeyStoreConstants.TRUSTSTORE_TYPE,
+ null);
+ final SslConfiguration sslConfig =
SslConfiguration.createSSLConfiguration(null, null, trustStoreConfig);
+ assertThatCode(() -> {
+ // noinspection EmptyTryBlock
+ try (final SslSocketManager ignored =
createSocketManager(sslConfig)) {
+ // Do nothing
+ }
+ })
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void host_name_verification_should_take_effect() {
+ final SslConfiguration sslConfig =
SslConfiguration.createSSLConfiguration(
+ null,
+ null,
+ null,
+ // Explicitly enable hostname verification
+ true);
+ try (final SslSocketManager ssm = createSocketManager(sslConfig)) {
+ final SSLSocket sslSocket = (SSLSocket) ssm.getSocket();
+ final SSLParameters sslParams = sslSocket.getSSLParameters();
+
assertThat(sslParams.getEndpointIdentificationAlgorithm()).isEqualTo("HTTPS");
+ assertThat(sslParams.getServerNames()).containsOnly(new
SNIHostName(HOST_NAME));
+ }
+ }
+
+ private static SslSocketManager createSocketManager(final SslConfiguration
sslConfig) {
+ return SslSocketManager.getSocketManager(
+ sslConfig, SslSocketManagerTest.HOST_NAME, HOST_PORT, 0, 0,
true, LAYOUT, 8192, null);
+ }
+
+ static String[] hostNamesAllowedForSniServerName() {
+ return new String[] {
+ "apache.org",
+ "a.b",
+ "sub.domain.co.uk",
+ "xn--bcher-kva.example", // valid IDN/punycode
+ "my-host-123.example",
+ "a1234567890.b1234567890.c1234567890", // numeric/alpha labels
+ "EXAMPLE.COM", // case-insensitive
+ "service--node.example", // double hyphens allowed
+ Strings.repeat("l", 63) + ".com", // 63-char label, valid
+ "a.b.c.d.e.f.g.h.i.j", // many labels, short each
+ };
+ }
+
+ @ParameterizedTest
+ @MethodSource("hostNamesAllowedForSniServerName")
+ void createSniHostName_should_work_with_allowed_input(String hostName) {
+ assertThat(createSniHostName(hostName)).isNotNull();
+ }
+
+ static String[] hostNamesNotAllowedForSniServerName() {
+ return new String[] {
+ // Invalid because IP literals not allowed
+ "192.168.1.1", // IPv4 literal
+ "2001:db8::1", // IPv6 literal
+ "[2001:db8::1]", // IPv6 URL literal form
+ // Invalid because illegal characters
+ "exa_mple.com", // underscore not allowed
+ "host name.com", // space not allowed
+ "example!.com", // symbol not allowed
+ // Invalid because label syntax violations
+ "-example.com", // leading hyphen
+ "example-.com", // trailing hyphen
+ ".example.com", // leading dot / empty label
+ // Invalid because label too long
+ Strings.repeat("l", 64) + ".com",
+ };
+ }
+
+ @ParameterizedTest
+ @MethodSource("hostNamesNotAllowedForSniServerName")
+ void createSniHostName_should_fail_with_not_allowed_input(String hostName)
{
+ assertThat(createSniHostName(hostName)).isNull();
+ }
+}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslSocketManagerTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslSocketManagerTest.java
deleted file mode 100644
index 85db9e8f0c..0000000000
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslSocketManagerTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.core.net.ssl;
-
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-
-import org.apache.logging.log4j.core.layout.PatternLayout;
-import org.apache.logging.log4j.core.net.SslSocketManager;
-import org.junit.jupiter.api.Test;
-import org.junitpioneer.jupiter.Issue;
-
-class SslSocketManagerTest {
- @Issue("https://github.com/apache/logging-log4j2/issues/3947")
- @Test
- void shouldNotThrowExceptionWhenConfiguringTrustStore() {
- final TrustStoreConfiguration trustStoreConfiguration =
assertDoesNotThrow(() -> new TrustStoreConfiguration(
- SslKeyStoreConstants.TRUSTSTORE_LOCATION,
- SslKeyStoreConstants::TRUSTSTORE_PWD,
- SslKeyStoreConstants.TRUSTSTORE_TYPE,
- null));
- final SslConfiguration sslConfiguration =
- SslConfiguration.createSSLConfiguration(null, null,
trustStoreConfiguration);
- assertDoesNotThrow(() -> {
- // noinspection EmptyTryBlock (try-with-resources to close
`SslSocketManager`, even on failure
- try (final SslSocketManager ignored =
SslSocketManager.getSocketManager(
- sslConfiguration, "localhost", 0, 0, 0, true,
PatternLayout.createDefaultLayout(), 8192, null)) {
- // Do nothing
- }
- });
- }
-}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SslSocketManager.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SslSocketManager.java
index 994f11bbce..56d283e936 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SslSocketManager.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SslSocketManager.java
@@ -30,19 +30,22 @@ import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
import org.apache.logging.log4j.util.Strings;
+import org.jspecify.annotations.Nullable;
-/**
- *
- */
public class SslSocketManager extends TcpSocketManager {
+
public static final int DEFAULT_PORT = 6514;
+
private static final SslSocketManagerFactory FACTORY = new
SslSocketManagerFactory();
+
private final SslConfiguration sslConfig;
/**
@@ -285,10 +288,7 @@ public class SslSocketManager extends TcpSocketManager {
@Override
protected Socket createSocket(final InetSocketAddress socketAddress)
throws IOException {
- final SSLSocketFactory socketFactory =
createSslSocketFactory(sslConfig);
- final Socket newSocket = socketFactory.createSocket();
- newSocket.connect(socketAddress, getConnectTimeoutMillis());
- return newSocket;
+ return createSocket(getHost(), socketAddress,
getConnectTimeoutMillis(), sslConfig, getSocketOptions());
}
private static SSLSocketFactory createSslSocketFactory(final
SslConfiguration sslConf) {
@@ -333,32 +333,102 @@ public class SslSocketManager extends TcpSocketManager {
for (InetSocketAddress socketAddress : socketAddresses) {
try {
return SslSocketManager.createSocket(
- socketAddress, data.connectTimeoutMillis,
data.sslConfiguration, data.socketOptions);
+ data.host,
+ socketAddress,
+ data.connectTimeoutMillis,
+ data.sslConfiguration,
+ data.socketOptions);
} catch (IOException ex) {
- ioe = ex;
+ final String message = String.format(
+ "failed create a socket to `%s:%s` that is
resolved to address `%s`",
+ data.host, data.port, socketAddress);
+ final IOException newEx = new IOException(message, ex);
+ if (ioe == null) {
+ ioe = newEx;
+ } else {
+ ioe.addSuppressed(newEx);
+ }
}
}
throw new IOException(errorMessage(data, socketAddresses), ioe);
}
}
- static Socket createSocket(
+ private static Socket createSocket(
+ final String hostName,
final InetSocketAddress socketAddress,
final int connectTimeoutMillis,
final SslConfiguration sslConfiguration,
final SocketOptions socketOptions)
throws IOException {
+
+ // Create the `SSLSocket`
final SSLSocketFactory socketFactory =
createSslSocketFactory(sslConfiguration);
final SSLSocket socket = (SSLSocket) socketFactory.createSocket();
+
+ // Apply socket options before `connect()`
if (socketOptions != null) {
- // Not sure which options must be applied before or after the
connect() call.
socketOptions.apply(socket);
}
+
+ // Connect the socket
socket.connect(socketAddress, connectTimeoutMillis);
- if (socketOptions != null) {
- // Not sure which options must be applied before or after the
connect() call.
- socketOptions.apply(socket);
+
+ // Verify the host name
+ if (sslConfiguration.isVerifyHostName()) {
+ // Allowed endpoint identification algorithms: HTTPS and LDAPS.
+ //
https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#endpoint-identification-algorithms
+ final SSLParameters sslParameters = socket.getSSLParameters();
+ sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+ final SNIHostName serverName = createSniHostName(hostName);
+ if (serverName != null) {
+
sslParameters.setServerNames(Collections.singletonList(serverName));
+ }
+ socket.setSSLParameters(sslParameters);
}
+
+ // Force the handshake right after `connect()` instead of waiting for
read/write to trigger it indirectly at a
+ // later stage
+ socket.startHandshake();
+
return socket;
}
+
+ /**
+ * {@return an {@link SNIHostName} instance if the provided host name is
not an IP literal (RFC 6066), and constitutes a valid host name (RFC 1035);
null otherwise}
+ *
+ * @param hostName a host name
+ *
+ * @see <a
href="https://www.rfc-editor.org/rfc/rfc6066.html#section-3">Literal IPv4 and
IPv6 addresses are not permitted in "HostName" (RFC 6066)</a>
+ * @see <a href="https://www.rfc-editor.org/rfc/rfc1035.html">Domain Names
- Implementation and Specification (RFC 1035)</a>
+ */
+ @Nullable
+ static SNIHostName createSniHostName(String hostName) {
+ // The actual check should be
+ //
+ // !isIPv4(h) && !isIPv6(h) && isValidHostName(h)
+ //
+ // Though we translate this into
+ //
+ // !h.matches("\d+[.]\d+[.]\d+[.]\d+") && new SNIServerName(h)
+ //
+ // This simplification is possible because
+ //
+ // - The `\d+[.]\d+[.]\d+[.]\d+` is sufficient to eliminate IPv4
addresses.
+ // Any sequence of four numeric labels (e.g., `1234.2345.3456.4567`)
is not a valid host name.
+ // Hence, false positives are not a problem, they would be
eliminated by `isValidHostName()` anyway.
+ //
+ // - `SNIServerName::new` throws an exception on invalid host names.
+ // This check is performed using `IDN.toASCII(hostName,
IDN.USE_STD3_ASCII_RULES)`.
+ // IPv6 literals don't qualify as a valid host name by
`IDN::toASCII`.
+ // This assumption on `IDN` is unlikely to change in the foreseeable
future.
+ if (!hostName.matches("\\d+[.]\\d+[.]\\d+[.]\\d+")) {
+ try {
+ return new SNIHostName(hostName);
+ } catch (IllegalArgumentException ignored) {
+ // Do nothing
+ }
+ }
+ return null;
+ }
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
index 41e2fc1031..a27febcabb 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
@@ -132,8 +132,8 @@ public class SslConfiguration {
@Nullable final TrustStoreConfiguration trustStoreConfig) {
try {
final SSLContext sslContext = SSLContext.getInstance(protocol);
- final KeyManager[] keyManagers = loadKeyManagers(keyStoreConfig);
- final TrustManager[] trustManagers =
loadTrustManagers(trustStoreConfig);
+ @Nullable final KeyManager[] keyManagers =
loadKeyManagers(keyStoreConfig);
+ @Nullable final TrustManager[] trustManagers =
loadTrustManagers(trustStoreConfig);
sslContext.init(keyManagers, trustManagers, null);
return sslContext;
} catch (final Exception error) {
@@ -144,9 +144,11 @@ public class SslConfiguration {
}
}
+ @Nullable
+ @NullUnmarked
private static KeyManager[] loadKeyManagers(@Nullable final
KeyStoreConfiguration config) throws Exception {
if (config == null) {
- return new KeyManager[0];
+ return null;
}
final KeyManagerFactory factory =
KeyManagerFactory.getInstance(config.getKeyManagerFactoryAlgorithm());
final char[] password = config.getPasswordAsCharArray();
@@ -158,9 +160,11 @@ public class SslConfiguration {
return factory.getKeyManagers();
}
+ @Nullable
+ @NullUnmarked
private static TrustManager[] loadTrustManagers(@Nullable final
TrustStoreConfiguration config) throws Exception {
if (config == null) {
- return new TrustManager[0];
+ return null;
}
final TrustManagerFactory factory =
TrustManagerFactory.getInstance(config.getTrustManagerFactoryAlgorithm());
factory.init(config.getKeyStore());
diff --git a/src/changelog/2.25.3/.release-notes.adoc.ftl
b/src/changelog/2.25.3/.release-notes.adoc.ftl
index 5e29b89f9e..23cd95914a 100644
--- a/src/changelog/2.25.3/.release-notes.adoc.ftl
+++ b/src/changelog/2.25.3/.release-notes.adoc.ftl
@@ -21,5 +21,7 @@
<#if release.date?has_content>Release date:: ${release.date}</#if>
This patch release addresses issues detailed in the changelog below.
+In particular, it includes an important fix for
xref:manual/appenders/network.adoc#SslConfiguration-attr-verifyHostName[the
host name verification in SSL/TLS configuration].
+This is used by xref:manual/appenders/network.adoc#SocketAppender[Socket
Appender].
<#include "../.changelog.adoc.ftl">
diff --git a/src/changelog/2.25.3/4002_fix_SslSocketAppender_verifyHostName.xml
b/src/changelog/2.25.3/4002_fix_SslSocketAppender_verifyHostName.xml
new file mode 100644
index 0000000000..a2d2ff1fd3
--- /dev/null
+++ b/src/changelog/2.25.3/4002_fix_SslSocketAppender_verifyHostName.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="fixed">
+ <issue id="4002" link="https://github.com/apache/logging-log4j2/pull/4002"/>
+ <description format="asciidoc">
+ Fix incorrect handling of
xref:manual/appenders/network.adoc#SslConfiguration-attr-verifyHostName[the
host name verification in SSL/TLS configuration], which is used by
xref:manual/appenders/network.adoc#SocketAppender[Socket Appender] when SSL/TLS
is enabled
+ </description>
+</entry>
diff --git a/src/site/antora/modules/ROOT/pages/manual/appenders/network.adoc
b/src/site/antora/modules/ROOT/pages/manual/appenders/network.adoc
index fee161916f..ef11d2dead 100644
--- a/src/site/antora/modules/ROOT/pages/manual/appenders/network.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/appenders/network.adoc
@@ -67,6 +67,9 @@
https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuid
If `true`, the host name in X509 certificate will be compared to the requested
host name.
In the case of a mismatch, the connection will fail.
+Note that SSL/TLS does not allow IP literals for host name verification – see
https://www.rfc-editor.org/rfc/rfc6066.html#section-3[RFC 6066].
+Host name verification will only be effective if the provided host name is not
an IP literal and a valid host name per
https://www.rfc-editor.org/rfc/rfc1035.html[RFC 1035].
+
See also
xref:manual/systemproperties.adoc#log4j2.sslVerifyHostName[`log4j2.sslVerifyHostName`].
|===