This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-net.git


The following commit(s) were added to refs/heads/master by this push:
     new a7972946 Run hostname verifier on FTPS data connection (#404)
a7972946 is described below

commit a79729464f6e69b773171ff30e3d483cab7afd70
Author: Javid Khan <[email protected]>
AuthorDate: Sat Jul 18 23:20:22 2026 +0530

    Run hostname verifier on FTPS data connection (#404)
    
    * run hostname verifier on FTPS data connection
    
    * Update FTPSClient.java
    
    ---------
    
    Co-authored-by: Gary Gregory <[email protected]>
---
 .../org/apache/commons/net/ftp/FTPSClient.java     |  3 +++
 .../apache/commons/net/ftp/AbstractFtpsTest.java   | 11 +++++++++
 .../org/apache/commons/net/ftp/FTPSClientTest.java | 27 ++++++++++++++++++++++
 3 files changed, 41 insertions(+)

diff --git a/src/main/java/org/apache/commons/net/ftp/FTPSClient.java 
b/src/main/java/org/apache/commons/net/ftp/FTPSClient.java
index 3ad7a841..15de27ff 100644
--- a/src/main/java/org/apache/commons/net/ftp/FTPSClient.java
+++ b/src/main/java/org/apache/commons/net/ftp/FTPSClient.java
@@ -299,6 +299,9 @@ public class FTPSClient extends FTPClient {
                 sslSocket.setEnabledProtocols(protocols);
             }
             sslSocket.startHandshake();
+            if (isClientMode && hostnameVerifier != null && 
!hostnameVerifier.verify(_hostname_, sslSocket.getSession())) {
+                throw new SSLHandshakeException("Hostname doesn't match 
certificate");
+            }
         }
 
         return socket;
diff --git a/src/test/java/org/apache/commons/net/ftp/AbstractFtpsTest.java 
b/src/test/java/org/apache/commons/net/ftp/AbstractFtpsTest.java
index ed1112bc..4c177147 100644
--- a/src/test/java/org/apache/commons/net/ftp/AbstractFtpsTest.java
+++ b/src/test/java/org/apache/commons/net/ftp/AbstractFtpsTest.java
@@ -27,6 +27,8 @@ import java.net.SocketException;
 import java.net.URL;
 import java.time.Duration;
 
+import javax.net.ssl.HostnameVerifier;
+
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.output.NullOutputStream;
 import org.apache.commons.lang3.ThreadUtils;
@@ -141,6 +143,8 @@ public abstract class AbstractFtpsTest {
 
     private boolean endpointCheckingEnabled;
 
+    private HostnameVerifier hostnameVerifier;
+
     protected void assertClientCode(final FTPSClient client) {
         final int replyCode = client.getReplyCode();
         assertTrue(FTPReply.isPositiveCompletion(replyCode));
@@ -169,6 +173,9 @@ public abstract class AbstractFtpsTest {
         assertEquals(62, client.getDataTimeout().getSeconds());
         //
         client.setEndpointCheckingEnabled(endpointCheckingEnabled);
+        if (hostnameVerifier != null) {
+            client.setHostnameVerifier(hostnameVerifier);
+        }
         client.connect("localhost", SocketPort);
         //
         assertClientCode(client);
@@ -207,4 +214,8 @@ public abstract class AbstractFtpsTest {
     public void setEndpointCheckingEnabled(final boolean value) {
         this.endpointCheckingEnabled = value;
     }
+
+    protected void setHostnameVerifier(final HostnameVerifier value) {
+        this.hostnameVerifier = value;
+    }
 }
diff --git a/src/test/java/org/apache/commons/net/ftp/FTPSClientTest.java 
b/src/test/java/org/apache/commons/net/ftp/FTPSClientTest.java
index b7c9ef0f..32e8eaaf 100644
--- a/src/test/java/org/apache/commons/net/ftp/FTPSClientTest.java
+++ b/src/test/java/org/apache/commons/net/ftp/FTPSClientTest.java
@@ -25,9 +25,11 @@ import java.io.IOException;
 import java.net.SocketException;
 import java.time.Instant;
 import java.util.Calendar;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Stream;
 
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -70,6 +72,31 @@ class FTPSClientTest extends AbstractFtpsTest {
         trace("<<testHasFeature");
     }
 
+    @Test
+    @Timeout(TEST_TIMEOUT)
+    void testHostnameVerifierUsedForDataConnection() throws SocketException, 
IOException {
+        final AtomicInteger verifyCount = new AtomicInteger();
+        setHostnameVerifier((hostname, session) -> {
+            verifyCount.incrementAndGet();
+            return true;
+        });
+        try {
+            final FTPSClient client = loginClient();
+            try {
+                // control connection has already been verified during login
+                final int afterControl = verifyCount.get();
+                assertTrue(afterControl >= 1, "verifier should run for the 
control connection");
+                // listFiles opens a data connection, which must be verified 
too
+                assertNotNull(client.listFiles(""));
+                assertTrue(verifyCount.get() > afterControl, "verifier should 
run for the data connection");
+            } finally {
+                client.disconnect();
+            }
+        } finally {
+            setHostnameVerifier(null);
+        }
+    }
+
     private void testListFiles(final String pathname) throws SocketException, 
IOException {
         final FTPSClient client = loginClient();
         try {

Reply via email to