Apache Tomcat: Client certificate verification bypass due to virtual host mapping
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.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.tomcat.embed:tomcat-embed-coreMaven | >= 11.0.0-M1, < 11.0.15 | 11.0.15 |
org.apache.tomcat.embed:tomcat-embed-coreMaven | >= 10.1.0-M1, < 10.1.50 | 10.1.50 |
org.apache.tomcat.embed:tomcat-embed-coreMaven | < 9.0.113 | 9.0.113 |
org.apache.tomcat:tomcatMaven | >= 11.0.0-M1, < 11.0.15 | 11.0.15 |
org.apache.tomcat:tomcatMaven | >= 10.1.0-M1, < 10.1.50 | 10.1.50 |
org.apache.tomcat:tomcatMaven | < 9.0.113 | 9.0.113 |
org.apache.tomcat:tomcat-catalinaMaven | >= 11.0.0-M1, < 11.0.15 | 11.0.15 |
org.apache.tomcat:tomcat-catalinaMaven | >= 10.1.0-M1, < 10.1.50 | 10.1.50 |
org.apache.tomcat:tomcat-catalinaMaven | < 9.0.113 | 9.0.113 |
Affected products
2- Apache Software Foundation/Apache Tomcatv5Range: 11.0.0-M1
Patches
61 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(); }
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:
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:
972f9a5e2a07Add protocol host name and SNI host name matching
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>
152c14885d45Add protocol host name and SNI host name matching
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>
258a591b61f8Add protocol host name and SNI host name matching
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- github.com/advisories/GHSA-fpj8-gq4v-p354ghsaADVISORY
- lists.apache.org/thread/vw6lxtlh2qbqwpb61wd3sv1flm2nttw7ghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2025-66614ghsaADVISORY
- github.com/apache/tomcat/commit/152c14885d45f5e0a8b59bd9f93c289cfe20ce30ghsaWEB
- github.com/apache/tomcat/commit/258a591b61f8cf5c22109e21e5a2a38b63454fd2ghsaWEB
- github.com/apache/tomcat/commit/5053fa82a1b2b52756810601227984a8b71888a4ghsaWEB
- github.com/apache/tomcat/commit/9276b5e783c8cd5b3fe2bb716306b65004bdd940ghsaWEB
- github.com/apache/tomcat/commit/972f9a5e2a07674d92610c478aac1b205d60724eghsaWEB
- github.com/apache/tomcat/commit/a4aa74232e826028cd2f7ba0445caf8a8b52c509ghsaWEB
- tomcat.apache.org/security-10.htmlghsaWEB
- tomcat.apache.org/security-11.htmlghsaWEB
- tomcat.apache.org/security-9.htmlghsaWEB
News mentions
0No linked articles in our index yet.