VYPR
Moderate severityNVD Advisory· Published Feb 17, 2026· Updated Mar 24, 2026

Apache Tomcat: Client certificate verification bypass due to virtual host mapping

CVE-2025-66614

Description

Improper Input Validation vulnerability.

This issue affects Apache Tomcat: from 11.0.0-M1 through 11.0.14, from 10.1.0-M1 through 10.1.49, from 9.0.0-M1 through 9.0.112.

The following versions were EOL at the time the CVE was created but are known to be affected: 8.5.0 through 8.5.100. Older EOL versions are not affected. Tomcat did not validate that the host name provided via the SNI extension was the same as the host name provided in the HTTP host header field. If Tomcat was configured with more than one virtual host and the TLS configuration for one of those hosts did not require client certificate authentication but another one did, it was possible for a client to bypass the client certificate authentication by sending different host names in the SNI extension and the HTTP host header field.

The vulnerability only applies if client certificate authentication is only enforced at the Connector. It does not apply if client certificate authentication is enforced at the web application.

Users are recommended to upgrade to version 11.0.15 or later, 10.1.50 or later or 9.0.113 or later, which fix the issue.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Apache Tomcat fails to validate SNI host name matches HTTP host header, allowing bypass of client certificate authentication in multi-host configurations.

Vulnerability

Description

Apache Tomcat does not validate that the host name provided via the TLS Server Name Indication (SNI) extension matches the host name in the HTTP Host header. This affects versions 11.0.0-M1 through 11.0.14, 10.1.0-M1 through 10.1.49, 9.0.0-M1 through 9.0.112, and the EOL 8.5.x branch (8.5.0 to 8.5.100) [1][2][3].

Exploitation

An attacker can exploit this by sending different host names in the SNI extension and the HTTP Host header. When Tomcat is configured with multiple virtual hosts, and one host requires TLS client certificate authentication while another does not, the client can bypass the authentication by directing the request to the unprotected host via the Host header while using the protected host's SNI name for the TLS handshake. This vulnerability only applies if client certificate authentication is enforced at the Connector level, not at the web application level [description].

Impact

Successful exploitation allows an attacker to bypass client certificate authentication, gaining unauthorized access to resources that should require certificate-based client identity verification.

Mitigation

Users should upgrade to Apache Tomcat 11.0.15 or later, 10.1.50 or later, or 9.0.113 or later, which include a fix that adds SNI-to-host header validation [1][2][3]. The fix is implemented in commit [4] which rejects requests where the host header does not match the SNI host.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.apache.tomcat.embed:tomcat-embed-coreMaven
>= 11.0.0-M1, < 11.0.1511.0.15
org.apache.tomcat.embed:tomcat-embed-coreMaven
>= 10.1.0-M1, < 10.1.5010.1.50
org.apache.tomcat.embed:tomcat-embed-coreMaven
< 9.0.1139.0.113
org.apache.tomcat:tomcatMaven
>= 11.0.0-M1, < 11.0.1511.0.15
org.apache.tomcat:tomcatMaven
>= 10.1.0-M1, < 10.1.5010.1.50
org.apache.tomcat:tomcatMaven
< 9.0.1139.0.113
org.apache.tomcat:tomcat-catalinaMaven
>= 11.0.0-M1, < 11.0.1511.0.15
org.apache.tomcat:tomcat-catalinaMaven
>= 10.1.0-M1, < 10.1.5010.1.50
org.apache.tomcat:tomcat-catalinaMaven
< 9.0.1139.0.113

Affected products

2
  • Apache/Tomcatllm-fuzzy
    Range: >=11.0.0-M1 <=11.0.14, >=10.1.0-M1 <=10.1.49, >=9.0.0-M1 <=9.0.112, >=8.5.0 <=8.5.100
  • Apache Software Foundation/Apache Tomcatv5
    Range: 11.0.0-M1

Patches

6
9276b5e783c8

Add support for APR

https://github.com/apache/tomcatremmDec 2, 2025via ghsa
1 file changed · +8 0
  • java/org/apache/tomcat/util/net/AprEndpoint.java+8 0 modified
    @@ -89,6 +89,8 @@ public class AprEndpoint extends AbstractEndpoint<Long,Long> implements SNICallB
         private static final Log log = LogFactory.getLog(AprEndpoint.class);
         private static final Log logCertificate = LogFactory.getLog(AprEndpoint.class.getName() + ".certificate");
     
    +    private static final ThreadLocal<SocketWrapperBase<Long>> localWrapper = new ThreadLocal<>();
    +
         // ----------------------------------------------------------------- Fields
     
         /**
    @@ -515,6 +517,10 @@ protected void createSSLContext(SSLHostConfig sslHostConfig) throws Exception {
     
         @Override
         public long getSslContext(String sniHostName) {
    +        SocketWrapperBase<Long> socketWrapper = localWrapper.get();
    +        if (socketWrapper != null) {
    +            socketWrapper.setSniHostName(sniHostName);
    +        }
             SSLHostConfig sslHostConfig = getSSLHostConfig(sniHostName);
             Long ctx = sslHostConfig.getOpenSslContext();
             if (ctx != null) {
    @@ -763,13 +769,15 @@ protected boolean setSocketOptions(SocketWrapperBase<Long> socketWrapper) {
                     WriteLock wl = ((AprSocketWrapper) socketWrapper).getBlockingStatusWriteLock();
                     wl.lock();
                     try {
    +                    localWrapper.set(socketWrapper);
                         if (SSLSocket.handshake(socket) != 0) {
                             if (log.isDebugEnabled()) {
                                 log.debug(sm.getString("endpoint.err.handshake") + ": " + SSL.getLastError());
                             }
                             return false;
                         }
                     } finally {
    +                    localWrapper.set(null);
                         wl.unlock();
                     }
     
    
a4aa74232e82

Add support for NIO2

https://github.com/apache/tomcatremmDec 1, 2025via ghsa
1 file changed · +1 0
  • java/org/apache/tomcat/util/net/SecureNio2Channel.java+1 0 modified
    @@ -410,6 +410,7 @@ private int processSNI() throws IOException {
             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:
    
5053fa82a1b2

Add support for NIO2

https://github.com/apache/tomcatremmDec 1, 2025via ghsa
1 file changed · +1 0
  • java/org/apache/tomcat/util/net/SecureNio2Channel.java+1 0 modified
    @@ -408,6 +408,7 @@ private int processSNI() throws IOException {
             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:
    
972f9a5e2a07

Add protocol host name and SNI host name matching

https://github.com/apache/tomcatremmDec 1, 2025via ghsa
13 files changed · +255 4
  • java/org/apache/coyote/http11/AbstractHttp11Protocol.java+4 0 modified
    @@ -792,6 +792,10 @@ public void reloadSslHostConfig(String hostName) {
         }
     
     
    +    public boolean checkSni(String sniHostName, String protocolHostName) {
    +        return getEndpoint().checkSni(sniHostName, protocolHostName);
    +    }
    +
         // ------------------------------------------------------------- Common code
     
         @Override
    
  • java/org/apache/coyote/http11/Http11Processor.java+5 0 modified
    @@ -792,6 +792,11 @@ private void prepareRequest() throws IOException {
             // 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);
             }
    
  • java/org/apache/coyote/http11/LocalStrings.properties+1 0 modified
    @@ -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
    
  • java/org/apache/coyote/http2/Http2UpgradeHandler.java+4 0 modified
    @@ -1942,6 +1942,10 @@ public ServletConnection getServletConnection() {
             }
         }
     
    +    String getSniHostName() {
    +        return socketWrapper.getSniHostName();
    +    }
    +
         protected class PingManager {
     
             protected boolean initiateDisabled = false;
    
  • java/org/apache/coyote/http2/LocalStrings.properties+1 0 modified
    @@ -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
    
  • java/org/apache/coyote/http2/Stream.java+5 0 modified
    @@ -511,6 +511,11 @@ private void parseAuthority(String value, boolean host) throws HpackException {
             } 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()));
    +        }
         }
     
     
    
  • java/org/apache/tomcat/util/net/AbstractEndpoint.java+27 0 modified
    @@ -221,6 +221,17 @@ public Set<SocketWrapperBase<S>> getConnections() {
     
         // ----------------------------------------------------------------- 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 @@ protected SSLHostConfig getSSLHostConfig(String sniHostName) {
         }
     
     
    +    /**
    +     * 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?
          */
    
  • java/org/apache/tomcat/util/net/SecureNioChannel.java+1 0 modified
    @@ -279,6 +279,7 @@ private int processSNI() throws IOException {
             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:
    
  • java/org/apache/tomcat/util/net/SocketWrapperBase.java+16 0 modified
    @@ -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 void setNegotiatedProtocol(String negotiatedProtocol) {
             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.
          *
    
  • test/org/apache/catalina/startup/SimpleHttpClient.java+14 4 modified
    @@ -205,18 +205,28 @@ public String getRedirectUri() {
             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);
         }
    
  • test/org/apache/tomcat/util/net/TestSsl.java+160 0 modified
    @@ -34,7 +34,10 @@
     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.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.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 void testKeyPassFile() throws Exception {
                     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 {
     
    
  • webapps/docs/changelog.xml+8 0 modified
    @@ -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">
    
  • webapps/docs/config/http.xml+9 0 modified
    @@ -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>
    
152c14885d45

Add protocol host name and SNI host name matching

https://github.com/apache/tomcatremmDec 1, 2025via ghsa
13 files changed · +255 4
  • java/org/apache/coyote/AbstractProtocol.java+4 0 modified
    @@ -490,6 +490,10 @@ public int getWaitingProcessorCount() {
         }
     
     
    +    public boolean checkSni(String sniHostName, String protocolHostName) {
    +        return getEndpoint().checkSni(sniHostName, protocolHostName);
    +    }
    +
         // ----------------------------------------------- Accessors for sub-classes
     
         protected AbstractEndpoint<S,?> getEndpoint() {
    
  • java/org/apache/coyote/http11/Http11Processor.java+5 0 modified
    @@ -791,6 +791,11 @@ private void prepareRequest() throws IOException {
             // 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);
             }
    
  • java/org/apache/coyote/http11/LocalStrings.properties+1 0 modified
    @@ -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
    
  • java/org/apache/coyote/http2/Http2UpgradeHandler.java+4 0 modified
    @@ -1931,6 +1931,10 @@ void replaceStream(AbstractNonZeroStream original, AbstractNonZeroStream replace
         }
     
     
    +    String getSniHostName() {
    +        return socketWrapper.getSniHostName();
    +    }
    +
         protected class PingManager {
     
             protected boolean initiateDisabled = false;
    
  • java/org/apache/coyote/http2/LocalStrings.properties+1 0 modified
    @@ -104,6 +104,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
    
  • java/org/apache/coyote/http2/Stream.java+5 0 modified
    @@ -509,6 +509,11 @@ private void parseAuthority(String value, boolean host) throws HpackException {
             } 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()));
    +        }
         }
     
     
    
  • java/org/apache/tomcat/util/net/AbstractEndpoint.java+27 0 modified
    @@ -232,6 +232,17 @@ public Set<SocketWrapperBase<S>> getConnections() {
     
         // ----------------------------------------------------------------- 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;
     
         /**
    @@ -525,6 +536,22 @@ protected SSLHostConfig getSSLHostConfig(String sniHostName) {
         }
     
     
    +    /**
    +     * 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?
          */
    
  • java/org/apache/tomcat/util/net/SecureNioChannel.java+1 0 modified
    @@ -281,6 +281,7 @@ private int processSNI() throws IOException {
             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:
    
  • java/org/apache/tomcat/util/net/SocketWrapperBase.java+16 0 modified
    @@ -72,6 +72,8 @@ public abstract class SocketWrapperBase<E> {
         protected String remoteHost = null;
         protected int remotePort = -1;
     
    +    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.
    @@ -234,6 +236,20 @@ public void setNegotiatedProtocol(String negotiatedProtocol) {
             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.
          *
    
  • test/org/apache/catalina/startup/SimpleHttpClient.java+14 4 modified
    @@ -197,19 +197,29 @@ public String getRedirectUri() {
             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 {
             final String encoding = "ISO-8859-1";
             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, encoding);
             InputStream is = socket.getInputStream();
             Reader r = new InputStreamReader(is, encoding);
             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);
         }
    
  • test/org/apache/tomcat/util/net/TestSsl.java+160 0 modified
    @@ -34,7 +34,10 @@
     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;
     import javax.servlet.ServletException;
    @@ -50,6 +53,7 @@
     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;
    @@ -58,11 +62,14 @@
     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;
     
    @@ -287,6 +294,159 @@ public void testKeyPassFile() throws Exception {
                     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 {
     
    
  • webapps/docs/changelog.xml+8 0 modified
    @@ -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">
    
  • webapps/docs/config/http.xml+9 0 modified
    @@ -327,6 +327,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>
    
258a591b61f8

Add protocol host name and SNI host name matching

https://github.com/apache/tomcatremmDec 1, 2025via ghsa
13 files changed · +255 4
  • java/org/apache/coyote/http11/AbstractHttp11Protocol.java+4 0 modified
    @@ -759,6 +759,10 @@ public void setSniParseLimit(int sniParseLimit) {
         }
     
     
    +    public boolean checkSni(String sniHostName, String protocolHostName) {
    +        return getEndpoint().checkSni(sniHostName, protocolHostName);
    +    }
    +
         // ------------------------------------------------------------- Common code
     
         @Override
    
  • java/org/apache/coyote/http11/Http11Processor.java+5 0 modified
    @@ -780,6 +780,11 @@ private void prepareRequest() throws IOException {
             // 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);
             }
    
  • java/org/apache/coyote/http11/LocalStrings.properties+1 0 modified
    @@ -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
    
  • java/org/apache/coyote/http2/Http2UpgradeHandler.java+4 0 modified
    @@ -1859,6 +1859,10 @@ public ServletConnection getServletConnection() {
             }
         }
     
    +    String getSniHostName() {
    +        return socketWrapper.getSniHostName();
    +    }
    +
         protected class PingManager {
     
             protected boolean initiateDisabled = false;
    
  • java/org/apache/coyote/http2/LocalStrings.properties+1 0 modified
    @@ -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
    
  • java/org/apache/coyote/http2/Stream.java+5 0 modified
    @@ -505,6 +505,11 @@ private void parseAuthority(String value, boolean host) throws HpackException {
             } 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()));
    +        }
         }
     
     
    
  • java/org/apache/tomcat/util/net/AbstractEndpoint.java+27 0 modified
    @@ -257,6 +257,17 @@ public void setSniParseLimit(int sniParseLimit) {
         }
     
     
    +    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;
     
         /**
    @@ -714,6 +725,22 @@ protected SSLHostConfig getSSLHostConfig(String sniHostName) {
         }
     
     
    +    /**
    +     * 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?
          */
    
  • java/org/apache/tomcat/util/net/SecureNioChannel.java+1 0 modified
    @@ -279,6 +279,7 @@ protected int processSNI() throws IOException {
             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:
    
  • java/org/apache/tomcat/util/net/SocketWrapperBase.java+16 0 modified
    @@ -85,6 +85,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.
    @@ -208,6 +210,20 @@ public void setNegotiatedProtocol(String negotiatedProtocol) {
             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.
          *
    
  • test/org/apache/catalina/startup/SimpleHttpClient.java+14 4 modified
    @@ -205,18 +205,28 @@ public String getRedirectUri() {
             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);
         }
    
  • test/org/apache/tomcat/util/net/TestSsl.java+160 0 modified
    @@ -34,7 +34,10 @@
     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.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.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 void testKeyPassFile() throws Exception {
                     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 {
     
    
  • webapps/docs/changelog.xml+8 0 modified
    @@ -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">
    
  • webapps/docs/config/http.xml+9 0 modified
    @@ -320,6 +320,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>
    

Vulnerability mechanics

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

References

12

News mentions

0

No linked articles in our index yet.