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

remm pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/10.1.x by this push:
     new 972f9a5e2a Add protocol host name and SNI host name matching
972f9a5e2a is described below

commit 972f9a5e2a07674d92610c478aac1b205d60724e
Author: remm <[email protected]>
AuthorDate: Mon Dec 1 21:16:00 2025 +0100

    Add protocol host name and SNI host name matching
    
    Add strictSNI attribute on the Connector to control it.
---
 .../coyote/http11/AbstractHttp11Protocol.java      |   4 +
 java/org/apache/coyote/http11/Http11Processor.java |   5 +
 .../apache/coyote/http11/LocalStrings.properties   |   1 +
 .../apache/coyote/http2/Http2UpgradeHandler.java   |   4 +
 .../apache/coyote/http2/LocalStrings.properties    |   1 +
 java/org/apache/coyote/http2/Stream.java           |   5 +
 .../apache/tomcat/util/net/AbstractEndpoint.java   |  27 ++++
 .../apache/tomcat/util/net/SecureNioChannel.java   |   1 +
 .../apache/tomcat/util/net/SocketWrapperBase.java  |  16 +++
 .../apache/catalina/startup/SimpleHttpClient.java  |  18 ++-
 test/org/apache/tomcat/util/net/TestSsl.java       | 160 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   8 ++
 webapps/docs/config/http.xml                       |   9 ++
 13 files changed, 255 insertions(+), 4 deletions(-)

diff --git a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java 
b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
index 8e5e0bf992..02ac4e589e 100644
--- a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
+++ b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
@@ -792,6 +792,10 @@ public abstract class AbstractHttp11Protocol<S> extends 
AbstractProtocol<S> {
     }
 
 
+    public boolean checkSni(String sniHostName, String protocolHostName) {
+        return getEndpoint().checkSni(sniHostName, protocolHostName);
+    }
+
     // ------------------------------------------------------------- Common 
code
 
     @Override
diff --git a/java/org/apache/coyote/http11/Http11Processor.java 
b/java/org/apache/coyote/http11/Http11Processor.java
index 4a9f5b8278..7cf96f5445 100644
--- a/java/org/apache/coyote/http11/Http11Processor.java
+++ b/java/org/apache/coyote/http11/Http11Processor.java
@@ -792,6 +792,11 @@ public class Http11Processor extends AbstractProcessor {
         // Validate host name and extract port if present
         parseHost(hostValueMB);
 
+        // Match host name with SNI if required
+        if (!protocol.checkSni(socketWrapper.getSniHostName(), 
request.serverName().toString())) {
+            badRequest("http11processor.request.sni");
+        }
+
         if (!getErrorState().isIoAllowed()) {
             getAdapter().log(request, response, 0);
         }
diff --git a/java/org/apache/coyote/http11/LocalStrings.properties 
b/java/org/apache/coyote/http11/LocalStrings.properties
index ccceb70765..7d9fe90f1f 100644
--- a/java/org/apache/coyote/http11/LocalStrings.properties
+++ b/java/org/apache/coyote/http11/LocalStrings.properties
@@ -39,6 +39,7 @@ http11processor.request.noHostHeader=The HTTP/1.1 request did 
not provide a host
 http11processor.request.nonNumericContentLength=The request contained a 
content-length header with a non-numeric value
 http11processor.request.prepare=Error preparing request
 http11processor.request.process=Error processing request
+http11processor.request.sni=The host header does not match the SNI host
 http11processor.request.unsupportedEncoding=Error preparing request, 
unsupported transfer encoding [{0}]
 http11processor.request.unsupportedVersion=Error preparing request, 
unsupported HTTP version [{0}]
 http11processor.response.finish=Error finishing response
diff --git a/java/org/apache/coyote/http2/Http2UpgradeHandler.java 
b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
index 4ac67284e3..c4e49ae8d0 100644
--- a/java/org/apache/coyote/http2/Http2UpgradeHandler.java
+++ b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
@@ -1942,6 +1942,10 @@ class Http2UpgradeHandler extends AbstractStream 
implements InternalHttpUpgradeH
         }
     }
 
+    String getSniHostName() {
+        return socketWrapper.getSniHostName();
+    }
+
     protected class PingManager {
 
         protected boolean initiateDisabled = false;
diff --git a/java/org/apache/coyote/http2/LocalStrings.properties 
b/java/org/apache/coyote/http2/LocalStrings.properties
index f7c2bc3e6a..111cc3d8b4 100644
--- a/java/org/apache/coyote/http2/LocalStrings.properties
+++ b/java/org/apache/coyote/http2/LocalStrings.properties
@@ -105,6 +105,7 @@ stream.header.te=Connection [{0}], Stream [{1}], HTTP 
header [te] is not permitt
 stream.header.unexpectedPseudoHeader=Connection [{0}], Stream [{1}], Pseudo 
header [{2}] received after a regular header
 stream.header.unknownPseudoHeader=Connection [{0}], Stream [{1}], Unknown 
pseudo header [{2}] received
 stream.host.inconsistent=Connection [{0}], Stream [{1}], The header host 
header [{2}] is inconsistent with previously provided values for host [{3}] 
and/or port [{4}]
+stream.host.sni=Connection [{0}], Stream [{1}], The host header [{2}] does not 
match the SNI host [{3}]
 stream.inputBuffer.copy=Copying [{0}] bytes from inBuffer to outBuffer
 stream.inputBuffer.dispatch=Data added to inBuffer when read interest is 
registered. Triggering a read dispatch
 stream.inputBuffer.empty=The Stream input buffer is empty. Waiting for more 
data
diff --git a/java/org/apache/coyote/http2/Stream.java 
b/java/org/apache/coyote/http2/Stream.java
index a39ebd1afe..6353095014 100644
--- a/java/org/apache/coyote/http2/Stream.java
+++ b/java/org/apache/coyote/http2/Stream.java
@@ -511,6 +511,11 @@ class Stream extends AbstractNonZeroStream implements 
HeaderEmitter {
         } else {
             coyoteRequest.serverName().setString(value);
         }
+        // Match host name with SNI if required
+        if 
(!handler.getProtocol().getHttp11Protocol().checkSni(handler.getSniHostName(), 
coyoteRequest.serverName().getString())) {
+            throw new HpackException(sm.getString("stream.host.sni", 
getConnectionId(), getIdAsString(), value,
+                    handler.getSniHostName()));
+        }
     }
 
 
diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java 
b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
index fc34c32a6d..ad85253b87 100644
--- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java
+++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
@@ -221,6 +221,17 @@ public abstract class AbstractEndpoint<S, U> {
 
     // ----------------------------------------------------------------- 
Properties
 
+    private boolean strictSni = true;
+
+    public boolean getStrictSni() {
+        return strictSni;
+    }
+
+    public void setStrictSni(boolean strictSni) {
+        this.strictSni = strictSni;
+    }
+
+
     private String defaultSSLHostConfigName = 
SSLHostConfig.DEFAULT_SSL_HOST_NAME;
 
     /**
@@ -508,6 +519,22 @@ public abstract class AbstractEndpoint<S, U> {
     }
 
 
+    /**
+     * Check if two host names share the same SSLHostConfig.
+     *
+     * @param sniHostName the host name from SNI, null if SNI is not in use
+     * @param protocolHostName the host name from the protocol
+     * @return true if SNI is not checked, if the SNI host name matches the 
protocol host name,
+     *    if both host names use the same SSLHostConfig configuration, if 
there is no SNI and the
+     *    protocol host name uses the default SSLHostConfig configuration, and 
false otherwise
+     */
+    public boolean checkSni(String sniHostName, String protocolHostName) {
+        return (!strictSni || !isSSLEnabled()
+                || (sniHostName != null && 
sniHostName.equalsIgnoreCase(protocolHostName))
+                || getSSLHostConfig(sniHostName) == 
getSSLHostConfig(protocolHostName));
+    }
+
+
     /**
      * Has the user requested that send file be used where possible?
      */
diff --git a/java/org/apache/tomcat/util/net/SecureNioChannel.java 
b/java/org/apache/tomcat/util/net/SecureNioChannel.java
index 6528b48cfe..b7273bf295 100644
--- a/java/org/apache/tomcat/util/net/SecureNioChannel.java
+++ b/java/org/apache/tomcat/util/net/SecureNioChannel.java
@@ -279,6 +279,7 @@ public class SecureNioChannel extends NioChannel {
         switch (extractor.getResult()) {
             case COMPLETE:
                 hostName = extractor.getSNIValue();
+                socketWrapper.setSniHostName(hostName);
                 clientRequestedApplicationProtocols = 
extractor.getClientRequestedApplicationProtocols();
                 //$FALL-THROUGH$ to set the client requested ciphers
             case NOT_PRESENT:
diff --git a/java/org/apache/tomcat/util/net/SocketWrapperBase.java 
b/java/org/apache/tomcat/util/net/SocketWrapperBase.java
index 714a8f0e2f..ec4f93955b 100644
--- a/java/org/apache/tomcat/util/net/SocketWrapperBase.java
+++ b/java/org/apache/tomcat/util/net/SocketWrapperBase.java
@@ -86,6 +86,8 @@ public abstract class SocketWrapperBase<E> {
     protected int remotePort = -1;
     protected volatile ServletConnection servletConnection = null;
 
+    protected String sniHostName = null;
+
     /**
      * Used to record the first IOException that occurs during non-blocking 
read/writes that can't be usefully
      * propagated up the stack since there is no user code or appropriate 
container code in the stack to handle it.
@@ -209,6 +211,20 @@ public abstract class SocketWrapperBase<E> {
         this.negotiatedProtocol = negotiatedProtocol;
     }
 
+    /**
+     * @return the sniHostName
+     */
+    public String getSniHostName() {
+        return this.sniHostName;
+    }
+
+    /**
+     * @param sniHostName the SNI host name to set
+     */
+    public void setSniHostName(String sniHostName) {
+        this.sniHostName = sniHostName;
+    }
+
     /**
      * Set the timeout for reading. Values of zero or less will be changed to 
-1.
      *
diff --git a/test/org/apache/catalina/startup/SimpleHttpClient.java 
b/test/org/apache/catalina/startup/SimpleHttpClient.java
index 70757f36b4..924f2e24e8 100644
--- a/test/org/apache/catalina/startup/SimpleHttpClient.java
+++ b/test/org/apache/catalina/startup/SimpleHttpClient.java
@@ -205,18 +205,28 @@ public abstract class SimpleHttpClient {
         return redirectUri;
     }
 
-    public void connect(int connectTimeout, int soTimeout)
-           throws UnknownHostException, IOException {
+    public void connect(Socket socket, int connectTimeout, int soTimeout, 
boolean connect) throws UnknownHostException, IOException {
         SocketAddress addr = new InetSocketAddress("localhost", port);
-        socket = new Socket();
+        this.socket = socket;
         socket.setSoTimeout(soTimeout);
-        socket.connect(addr,connectTimeout);
+        if (connect) {
+            socket.connect(addr, connectTimeout);
+        }
         OutputStream os = createOutputStream(socket);
         writer = new OutputStreamWriter(os, requestBodyEncoding);
         InputStream is = socket.getInputStream();
         Reader r = new InputStreamReader(is, responseBodyEncoding);
         reader = new BufferedReader(r);
     }
+
+    public void connect(int connectTimeout, int soTimeout) throws 
UnknownHostException, IOException {
+        connect(new Socket(), 10000, 10000, true);
+    }
+
+    public void connect(Socket socket) throws UnknownHostException, 
IOException {
+        connect(socket, 10000, 10000, false);
+    }
+
     public void connect() throws UnknownHostException, IOException {
         connect(10000, 10000);
     }
diff --git a/test/org/apache/tomcat/util/net/TestSsl.java 
b/test/org/apache/tomcat/util/net/TestSsl.java
index d17934876d..2e006b3bb3 100644
--- a/test/org/apache/tomcat/util/net/TestSsl.java
+++ b/test/org/apache/tomcat/util/net/TestSsl.java
@@ -34,7 +34,10 @@ import java.util.concurrent.atomic.AtomicInteger;
 import javax.net.SocketFactory;
 import javax.net.ssl.HandshakeCompletedEvent;
 import javax.net.ssl.HandshakeCompletedListener;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
 import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 
@@ -51,6 +54,7 @@ import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 
+import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
 import org.apache.catalina.Context;
 import org.apache.catalina.Lifecycle;
 import org.apache.catalina.LifecycleEvent;
@@ -59,11 +63,14 @@ import org.apache.catalina.Wrapper;
 import org.apache.catalina.connector.Connector;
 import org.apache.catalina.connector.Request;
 import org.apache.catalina.connector.Response;
+import org.apache.catalina.startup.SimpleHttpClient;
 import org.apache.catalina.startup.TesterServlet;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
 import org.apache.catalina.valves.ValveBase;
 import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
+import org.apache.tomcat.util.net.TesterSupport.ClientSSLSocketFactory;
 import org.apache.tomcat.util.net.openssl.OpenSSLStatus;
 import org.apache.tomcat.websocket.server.WsContextListener;
 
@@ -288,6 +295,159 @@ public class TestSsl extends TomcatBaseTest {
                 TesterSupport.getLastClientAuthRequestedIssuerCount() == 0);
     }
 
+    @Test
+    public void testSni() throws Exception {
+        System.setProperty("jsse.enableSNIExtension", "true");
+        ClientSSLSocketFactory clientSSLSocketFactory = 
TesterSupport.configureClientSsl();
+        Tomcat tomcat = getTomcatInstance();
+        tomcat.getConnector().setProperty("strictSni", "true");
+
+        File appDir = new File(getBuildDirectory(), "webapps/examples");
+        Context ctxt  = tomcat.addWebapp(null, "/examples", 
appDir.getAbsolutePath());
+        ctxt.addApplicationListener(WsContextListener.class.getName());
+
+        TesterSupport.initSsl(tomcat);
+
+        // Add another config for localhost
+        SSLHostConfig sslHostConfig = new SSLHostConfig();
+        SSLHostConfigCertificate certificate = new 
SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
+        sslHostConfig.addCertificate(certificate);
+        certificate.setCertificateKeystoreFile(new 
File(TesterSupport.LOCALHOST_RSA_JKS).getAbsolutePath());
+        certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS);
+        sslHostConfig.setHostName("localhost");
+        tomcat.getConnector().addSslHostConfig(sslHostConfig);
+
+        // Add another config for foobar
+        sslHostConfig = new SSLHostConfig();
+        certificate = new SSLHostConfigCertificate(sslHostConfig, 
Type.UNDEFINED);
+        sslHostConfig.addCertificate(certificate);
+        certificate.setCertificateKeystoreFile(new 
File(TesterSupport.LOCALHOST_RSA_JKS).getAbsolutePath());
+        certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS);
+        sslHostConfig.setHostName("foobar");
+        tomcat.getConnector().addSslHostConfig(sslHostConfig);
+
+        TesterSupport.configureSSLImplementation(tomcat, 
sslImplementationName, useOpenSSL);
+
+        tomcat.start();
+
+        // Send SNI and it matches
+        SSLSocket sslSocket = (SSLSocket) 
clientSSLSocketFactory.createSocket("localhost", getPort());
+        SNIHostName serverName = new SNIHostName("localhost");
+        List<SNIServerName> serverNames = new ArrayList<>(1);
+        serverNames.add(serverName);
+        SSLParameters params = sslSocket.getSSLParameters();
+        params.setServerNames(serverNames);
+        sslSocket.setSSLParameters(params);
+
+        Client client = new Client();
+        client.setPort(getPort());
+
+        // @formatter:off
+        client.setRequest(new String[] {
+                "GET /examples/servlets/servlet/HelloWorldExample HTTP/1.1" + 
CRLF +
+                    "Host: localhost" + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF
+                });
+        // @formatter:on
+        client.connect(sslSocket);
+        client.processRequest(true);
+
+        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+        Assert.assertTrue(client.getResponseBody().contains("<a 
href=\"../helloworld.html\">"));
+        client.disconnect();
+        client.reset();
+
+        // Send SNI and it does not match
+        sslSocket = (SSLSocket) 
clientSSLSocketFactory.createSocket("localhost", getPort());
+        params = sslSocket.getSSLParameters();
+        params.setServerNames(serverNames);
+        sslSocket.setSSLParameters(params);
+
+        // @formatter:off
+        client.setRequest(new String[] {
+                "GET /examples/servlets/servlet/HelloWorldExample HTTP/1.1" + 
CRLF +
+                    "Host: foobar" + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF
+                });
+        // @formatter:on
+        client.connect(sslSocket);
+        client.processRequest(true);
+
+        Assert.assertEquals(HttpServletResponse.SC_BAD_REQUEST, 
client.getStatusCode());
+        client.disconnect();
+        client.reset();
+
+        // Send SNI and it does not match, but this goes to the default host 
which is the same one
+        tomcat.getConnector().setProperty("defaultSSLHostConfigName", 
"localhost");
+        sslSocket = (SSLSocket) 
clientSSLSocketFactory.createSocket("localhost", getPort());
+        params = sslSocket.getSSLParameters();
+        params.setServerNames(serverNames);
+        sslSocket.setSSLParameters(params);
+
+        // @formatter:off
+        client.setRequest(new String[] {
+                "GET /examples/servlets/servlet/HelloWorldExample HTTP/1.1" + 
CRLF +
+                    "Host: something" + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF
+                });
+        // @formatter:on
+        client.connect(sslSocket);
+        client.processRequest(true);
+
+        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+        client.disconnect();
+        client.reset();
+        tomcat.getConnector().setProperty("defaultSSLHostConfigName", 
"_default_");
+
+        tomcat.getConnector().setProperty("strictSni", "false");
+
+        // SNI is not verified
+        sslSocket = (SSLSocket) 
clientSSLSocketFactory.createSocket("localhost", getPort());
+        params = sslSocket.getSSLParameters();
+        params.setServerNames(serverNames);
+        sslSocket.setSSLParameters(params);
+
+        // @formatter:off
+        client.setRequest(new String[] {
+                "GET /examples/servlets/servlet/HelloWorldExample HTTP/1.1" + 
CRLF +
+                    "Host: foobar" + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF
+                });
+        // @formatter:on
+        client.connect(sslSocket);
+        client.processRequest(true);
+
+        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+        client.disconnect();
+        client.reset();
+
+        tomcat.getConnector().setProperty("strictSni", "true");
+
+        // No SNI but this is the default config
+        tomcat.getConnector().setProperty("defaultSSLHostConfigName", 
"localhost");
+        Assert.assertEquals(HttpServletResponse.SC_OK,
+                getUrl("https://localhost:"; + getPort() + 
"/examples/servlets/servlet/HelloWorldExample", new ByteChunk(), null));
+
+        // No SNI and this is not the default config
+        tomcat.getConnector().setProperty("defaultSSLHostConfigName", 
"_default_");
+        Assert.assertEquals(HttpServletResponse.SC_BAD_REQUEST,
+                getUrl("https://localhost:"; + getPort() + 
"/examples/servlets/servlet/HelloWorldExample", new ByteChunk(), null));
+
+    }
+
+    private static final class Client extends SimpleHttpClient {
+
+        @Override
+        public boolean isResponseBodyOK() {
+            return true;
+        }
+
+    }
+
     @Test
     public void testClientInitiatedRenegotiation() throws Exception {
 
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index f2a0a3ac7c..665c9bdde7 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -138,6 +138,14 @@
       <fix>
         HTTP/0.9 only allows GET as the HTTP method. (remm)
       </fix>
+      <add>
+        Add <code>strictSNI</code> attribute on the <code>Connector</code> to
+        allow matching the <code>SSLHostConfig</code> configuration associated
+        with the SNI host name to the <code>SSLHostConfig</code> configuration
+        matched from the HTTP protocol host name. Non matching configurations
+        will cause the request to be rejected. The attribute default value is
+        <code>true</code>, enabling the matching. (remm)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Jasper">
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 7ff853907d..ecd3c55b79 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -328,6 +328,15 @@
       The default value is <code>false</code>.</p>
     </attribute>
 
+    <attribute name="strictSni" required="false">
+      <p>Set this attribute to <code>true</code> to verify that the
+      <code>SSLHostConfig</code> configuration associated with the SNI host 
name
+      is the same as the <code>SSLHostConfig</code> configuration associated
+      with the HTTP protocol virtual host name in use. Non matching requests
+      will be rejected.
+      The default value is <code>true</code>.</p>
+    </attribute>
+
     <attribute name="URIEncoding" required="false">
       <p>This specifies the character encoding used to decode the URI bytes,
       after %xx decoding the URL. The default value is <code>UTF-8</code>.</p>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to