This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/11.0.x by this push:
new 258a591b61 Add protocol host name and SNI host name matching
258a591b61 is described below
commit 258a591b61f8cf5c22109e21e5a2a38b63454fd2
Author: remm <[email protected]>
AuthorDate: Mon Dec 1 21:16:00 2025 +0100
Add protocol host name and SNI host name matching
Add strictSNI attribute on the Connector to control it.
---
.../coyote/http11/AbstractHttp11Protocol.java | 4 +
java/org/apache/coyote/http11/Http11Processor.java | 5 +
.../apache/coyote/http11/LocalStrings.properties | 1 +
.../apache/coyote/http2/Http2UpgradeHandler.java | 4 +
.../apache/coyote/http2/LocalStrings.properties | 1 +
java/org/apache/coyote/http2/Stream.java | 5 +
.../apache/tomcat/util/net/AbstractEndpoint.java | 27 ++++
.../apache/tomcat/util/net/SecureNioChannel.java | 1 +
.../apache/tomcat/util/net/SocketWrapperBase.java | 16 +++
.../apache/catalina/startup/SimpleHttpClient.java | 18 ++-
test/org/apache/tomcat/util/net/TestSsl.java | 160 +++++++++++++++++++++
webapps/docs/changelog.xml | 8 ++
webapps/docs/config/http.xml | 9 ++
13 files changed, 255 insertions(+), 4 deletions(-)
diff --git a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
index 0b876e6543..3b778a5057 100644
--- a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
+++ b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
@@ -759,6 +759,10 @@ public abstract class AbstractHttp11Protocol<S> extends
AbstractProtocol<S> {
}
+ public boolean checkSni(String sniHostName, String protocolHostName) {
+ return getEndpoint().checkSni(sniHostName, protocolHostName);
+ }
+
// ------------------------------------------------------------- Common
code
@Override
diff --git a/java/org/apache/coyote/http11/Http11Processor.java
b/java/org/apache/coyote/http11/Http11Processor.java
index e0af9ddb4a..61b2c72c63 100644
--- a/java/org/apache/coyote/http11/Http11Processor.java
+++ b/java/org/apache/coyote/http11/Http11Processor.java
@@ -780,6 +780,11 @@ public class Http11Processor extends AbstractProcessor {
// Validate host name and extract port if present
parseHost(hostValueMB);
+ // Match host name with SNI if required
+ if (!protocol.checkSni(socketWrapper.getSniHostName(),
request.serverName().toString())) {
+ badRequest("http11processor.request.sni");
+ }
+
if (!getErrorState().isIoAllowed()) {
getAdapter().log(request, response, 0);
}
diff --git a/java/org/apache/coyote/http11/LocalStrings.properties
b/java/org/apache/coyote/http11/LocalStrings.properties
index 7a1eaff93e..a930f68fc3 100644
--- a/java/org/apache/coyote/http11/LocalStrings.properties
+++ b/java/org/apache/coyote/http11/LocalStrings.properties
@@ -39,6 +39,7 @@ http11processor.request.noHostHeader=The HTTP/1.1 request did
not provide a host
http11processor.request.nonNumericContentLength=The request contained a
content-length header with a non-numeric value
http11processor.request.prepare=Error preparing request
http11processor.request.process=Error processing request
+http11processor.request.sni=The host header does not match the SNI host
http11processor.request.unsupportedEncoding=Error preparing request,
unsupported transfer encoding [{0}]
http11processor.request.unsupportedVersion=Error preparing request,
unsupported HTTP version [{0}]
http11processor.response.finish=Error finishing response
diff --git a/java/org/apache/coyote/http2/Http2UpgradeHandler.java
b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
index 0b56066050..ba7cc175bc 100644
--- a/java/org/apache/coyote/http2/Http2UpgradeHandler.java
+++ b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
@@ -1859,6 +1859,10 @@ class Http2UpgradeHandler extends AbstractStream
implements InternalHttpUpgradeH
}
}
+ String getSniHostName() {
+ return socketWrapper.getSniHostName();
+ }
+
protected class PingManager {
protected boolean initiateDisabled = false;
diff --git a/java/org/apache/coyote/http2/LocalStrings.properties
b/java/org/apache/coyote/http2/LocalStrings.properties
index cfc072beec..8726f49544 100644
--- a/java/org/apache/coyote/http2/LocalStrings.properties
+++ b/java/org/apache/coyote/http2/LocalStrings.properties
@@ -105,6 +105,7 @@ stream.header.te=Connection [{0}], Stream [{1}], HTTP
header [te] is not permitt
stream.header.unexpectedPseudoHeader=Connection [{0}], Stream [{1}], Pseudo
header [{2}] received after a regular header
stream.header.unknownPseudoHeader=Connection [{0}], Stream [{1}], Unknown
pseudo header [{2}] received
stream.host.inconsistent=Connection [{0}], Stream [{1}], The header host
header [{2}] is inconsistent with previously provided values for host [{3}]
and/or port [{4}]
+stream.host.sni=Connection [{0}], Stream [{1}], The host header [{2}] does not
match the SNI host [{3}]
stream.inputBuffer.copy=Copying [{0}] bytes from inBuffer to outBuffer
stream.inputBuffer.dispatch=Data added to inBuffer when read interest is
registered. Triggering a read dispatch
stream.inputBuffer.empty=The Stream input buffer is empty. Waiting for more
data
diff --git a/java/org/apache/coyote/http2/Stream.java
b/java/org/apache/coyote/http2/Stream.java
index 61966dea0f..d50d2135de 100644
--- a/java/org/apache/coyote/http2/Stream.java
+++ b/java/org/apache/coyote/http2/Stream.java
@@ -505,6 +505,11 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
} else {
coyoteRequest.serverName().setString(value);
}
+ // Match host name with SNI if required
+ if
(!handler.getProtocol().getHttp11Protocol().checkSni(handler.getSniHostName(),
coyoteRequest.serverName().getString())) {
+ throw new HpackException(sm.getString("stream.host.sni",
getConnectionId(), getIdAsString(), value,
+ handler.getSniHostName()));
+ }
}
diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java
b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
index 866f638a8e..791879276a 100644
--- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java
+++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
@@ -257,6 +257,17 @@ public abstract class AbstractEndpoint<S, U> {
}
+ 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 @@ public abstract class AbstractEndpoint<S, U> {
}
+ /**
+ * Check if two host names share the same SSLHostConfig.
+ *
+ * @param sniHostName the host name from SNI, null if SNI is not in use
+ * @param protocolHostName the host name from the protocol
+ * @return true if SNI is not checked, if the SNI host name matches the
protocol host name,
+ * if both host names use the same SSLHostConfig configuration, if
there is no SNI and the
+ * protocol host name uses the default SSLHostConfig configuration, and
false otherwise
+ */
+ public boolean checkSni(String sniHostName, String protocolHostName) {
+ return (!strictSni || !isSSLEnabled()
+ || (sniHostName != null &&
sniHostName.equalsIgnoreCase(protocolHostName))
+ || getSSLHostConfig(sniHostName) ==
getSSLHostConfig(protocolHostName));
+ }
+
+
/**
* Has the user requested that send file be used where possible?
*/
diff --git a/java/org/apache/tomcat/util/net/SecureNioChannel.java
b/java/org/apache/tomcat/util/net/SecureNioChannel.java
index 4bf40a7796..af80209cbf 100644
--- a/java/org/apache/tomcat/util/net/SecureNioChannel.java
+++ b/java/org/apache/tomcat/util/net/SecureNioChannel.java
@@ -279,6 +279,7 @@ public class SecureNioChannel extends NioChannel {
switch (extractor.getResult()) {
case COMPLETE:
hostName = extractor.getSNIValue();
+ socketWrapper.setSniHostName(hostName);
clientRequestedApplicationProtocols =
extractor.getClientRequestedApplicationProtocols();
//$FALL-THROUGH$ to set the client requested ciphers
case NOT_PRESENT:
diff --git a/java/org/apache/tomcat/util/net/SocketWrapperBase.java
b/java/org/apache/tomcat/util/net/SocketWrapperBase.java
index 1b4f2eda8c..4e54d7a264 100644
--- a/java/org/apache/tomcat/util/net/SocketWrapperBase.java
+++ b/java/org/apache/tomcat/util/net/SocketWrapperBase.java
@@ -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 abstract class SocketWrapperBase<E> {
this.negotiatedProtocol = negotiatedProtocol;
}
+ /**
+ * @return the sniHostName
+ */
+ public String getSniHostName() {
+ return this.sniHostName;
+ }
+
+ /**
+ * @param sniHostName the SNI host name to set
+ */
+ public void setSniHostName(String sniHostName) {
+ this.sniHostName = sniHostName;
+ }
+
/**
* Set the timeout for reading. Values of zero or less will be changed to
-1.
*
diff --git a/test/org/apache/catalina/startup/SimpleHttpClient.java
b/test/org/apache/catalina/startup/SimpleHttpClient.java
index 70757f36b4..924f2e24e8 100644
--- a/test/org/apache/catalina/startup/SimpleHttpClient.java
+++ b/test/org/apache/catalina/startup/SimpleHttpClient.java
@@ -205,18 +205,28 @@ public abstract class SimpleHttpClient {
return redirectUri;
}
- public void connect(int connectTimeout, int soTimeout)
- throws UnknownHostException, IOException {
+ public void connect(Socket socket, int connectTimeout, int soTimeout,
boolean connect) throws UnknownHostException, IOException {
SocketAddress addr = new InetSocketAddress("localhost", port);
- socket = new Socket();
+ this.socket = socket;
socket.setSoTimeout(soTimeout);
- socket.connect(addr,connectTimeout);
+ if (connect) {
+ socket.connect(addr, connectTimeout);
+ }
OutputStream os = createOutputStream(socket);
writer = new OutputStreamWriter(os, requestBodyEncoding);
InputStream is = socket.getInputStream();
Reader r = new InputStreamReader(is, responseBodyEncoding);
reader = new BufferedReader(r);
}
+
+ public void connect(int connectTimeout, int soTimeout) throws
UnknownHostException, IOException {
+ connect(new Socket(), 10000, 10000, true);
+ }
+
+ public void connect(Socket socket) throws UnknownHostException,
IOException {
+ connect(socket, 10000, 10000, false);
+ }
+
public void connect() throws UnknownHostException, IOException {
connect(10000, 10000);
}
diff --git a/test/org/apache/tomcat/util/net/TestSsl.java
b/test/org/apache/tomcat/util/net/TestSsl.java
index d17934876d..2e006b3bb3 100644
--- a/test/org/apache/tomcat/util/net/TestSsl.java
+++ b/test/org/apache/tomcat/util/net/TestSsl.java
@@ -34,7 +34,10 @@ import java.util.concurrent.atomic.AtomicInteger;
import javax.net.SocketFactory;
import javax.net.ssl.HandshakeCompletedEvent;
import javax.net.ssl.HandshakeCompletedListener;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
@@ -51,6 +54,7 @@ import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
+import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
@@ -59,11 +63,14 @@ import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
+import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.TesterServlet;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.catalina.valves.ValveBase;
import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
+import org.apache.tomcat.util.net.TesterSupport.ClientSSLSocketFactory;
import org.apache.tomcat.util.net.openssl.OpenSSLStatus;
import org.apache.tomcat.websocket.server.WsContextListener;
@@ -288,6 +295,159 @@ public class TestSsl extends TomcatBaseTest {
TesterSupport.getLastClientAuthRequestedIssuerCount() == 0);
}
+ @Test
+ public void testSni() throws Exception {
+ System.setProperty("jsse.enableSNIExtension", "true");
+ ClientSSLSocketFactory clientSSLSocketFactory =
TesterSupport.configureClientSsl();
+ Tomcat tomcat = getTomcatInstance();
+ tomcat.getConnector().setProperty("strictSni", "true");
+
+ File appDir = new File(getBuildDirectory(), "webapps/examples");
+ Context ctxt = tomcat.addWebapp(null, "/examples",
appDir.getAbsolutePath());
+ ctxt.addApplicationListener(WsContextListener.class.getName());
+
+ TesterSupport.initSsl(tomcat);
+
+ // Add another config for localhost
+ SSLHostConfig sslHostConfig = new SSLHostConfig();
+ SSLHostConfigCertificate certificate = new
SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
+ sslHostConfig.addCertificate(certificate);
+ certificate.setCertificateKeystoreFile(new
File(TesterSupport.LOCALHOST_RSA_JKS).getAbsolutePath());
+ certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS);
+ sslHostConfig.setHostName("localhost");
+ tomcat.getConnector().addSslHostConfig(sslHostConfig);
+
+ // Add another config for foobar
+ sslHostConfig = new SSLHostConfig();
+ certificate = new SSLHostConfigCertificate(sslHostConfig,
Type.UNDEFINED);
+ sslHostConfig.addCertificate(certificate);
+ certificate.setCertificateKeystoreFile(new
File(TesterSupport.LOCALHOST_RSA_JKS).getAbsolutePath());
+ certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS);
+ sslHostConfig.setHostName("foobar");
+ tomcat.getConnector().addSslHostConfig(sslHostConfig);
+
+ TesterSupport.configureSSLImplementation(tomcat,
sslImplementationName, useOpenSSL);
+
+ tomcat.start();
+
+ // Send SNI and it matches
+ SSLSocket sslSocket = (SSLSocket)
clientSSLSocketFactory.createSocket("localhost", getPort());
+ SNIHostName serverName = new SNIHostName("localhost");
+ List<SNIServerName> serverNames = new ArrayList<>(1);
+ serverNames.add(serverName);
+ SSLParameters params = sslSocket.getSSLParameters();
+ params.setServerNames(serverNames);
+ sslSocket.setSSLParameters(params);
+
+ Client client = new Client();
+ client.setPort(getPort());
+
+ // @formatter:off
+ client.setRequest(new String[] {
+ "GET /examples/servlets/servlet/HelloWorldExample HTTP/1.1" +
CRLF +
+ "Host: localhost" + CRLF +
+ "Connection: Close" + CRLF +
+ CRLF
+ });
+ // @formatter:on
+ client.connect(sslSocket);
+ client.processRequest(true);
+
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("<a
href=\"../helloworld.html\">"));
+ client.disconnect();
+ client.reset();
+
+ // Send SNI and it does not match
+ sslSocket = (SSLSocket)
clientSSLSocketFactory.createSocket("localhost", getPort());
+ params = sslSocket.getSSLParameters();
+ params.setServerNames(serverNames);
+ sslSocket.setSSLParameters(params);
+
+ // @formatter:off
+ client.setRequest(new String[] {
+ "GET /examples/servlets/servlet/HelloWorldExample HTTP/1.1" +
CRLF +
+ "Host: foobar" + CRLF +
+ "Connection: Close" + CRLF +
+ CRLF
+ });
+ // @formatter:on
+ client.connect(sslSocket);
+ client.processRequest(true);
+
+ Assert.assertEquals(HttpServletResponse.SC_BAD_REQUEST,
client.getStatusCode());
+ client.disconnect();
+ client.reset();
+
+ // Send SNI and it does not match, but this goes to the default host
which is the same one
+ tomcat.getConnector().setProperty("defaultSSLHostConfigName",
"localhost");
+ sslSocket = (SSLSocket)
clientSSLSocketFactory.createSocket("localhost", getPort());
+ params = sslSocket.getSSLParameters();
+ params.setServerNames(serverNames);
+ sslSocket.setSSLParameters(params);
+
+ // @formatter:off
+ client.setRequest(new String[] {
+ "GET /examples/servlets/servlet/HelloWorldExample HTTP/1.1" +
CRLF +
+ "Host: something" + CRLF +
+ "Connection: Close" + CRLF +
+ CRLF
+ });
+ // @formatter:on
+ client.connect(sslSocket);
+ client.processRequest(true);
+
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ client.disconnect();
+ client.reset();
+ tomcat.getConnector().setProperty("defaultSSLHostConfigName",
"_default_");
+
+ tomcat.getConnector().setProperty("strictSni", "false");
+
+ // SNI is not verified
+ sslSocket = (SSLSocket)
clientSSLSocketFactory.createSocket("localhost", getPort());
+ params = sslSocket.getSSLParameters();
+ params.setServerNames(serverNames);
+ sslSocket.setSSLParameters(params);
+
+ // @formatter:off
+ client.setRequest(new String[] {
+ "GET /examples/servlets/servlet/HelloWorldExample HTTP/1.1" +
CRLF +
+ "Host: foobar" + CRLF +
+ "Connection: Close" + CRLF +
+ CRLF
+ });
+ // @formatter:on
+ client.connect(sslSocket);
+ client.processRequest(true);
+
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ client.disconnect();
+ client.reset();
+
+ tomcat.getConnector().setProperty("strictSni", "true");
+
+ // No SNI but this is the default config
+ tomcat.getConnector().setProperty("defaultSSLHostConfigName",
"localhost");
+ Assert.assertEquals(HttpServletResponse.SC_OK,
+ getUrl("https://localhost:" + getPort() +
"/examples/servlets/servlet/HelloWorldExample", new ByteChunk(), null));
+
+ // No SNI and this is not the default config
+ tomcat.getConnector().setProperty("defaultSSLHostConfigName",
"_default_");
+ Assert.assertEquals(HttpServletResponse.SC_BAD_REQUEST,
+ getUrl("https://localhost:" + getPort() +
"/examples/servlets/servlet/HelloWorldExample", new ByteChunk(), null));
+
+ }
+
+ private static final class Client extends SimpleHttpClient {
+
+ @Override
+ public boolean isResponseBodyOK() {
+ return true;
+ }
+
+ }
+
@Test
public void testClientInitiatedRenegotiation() throws Exception {
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 25e72bbfe6..e8fdb28a2b 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -138,6 +138,14 @@
<fix>
HTTP/0.9 only allows GET as the HTTP method. (remm)
</fix>
+ <add>
+ Add <code>strictSNI</code> attribute on the <code>Connector</code> to
+ allow matching the <code>SSLHostConfig</code> configuration associated
+ with the SNI host name to the <code>SSLHostConfig</code> configuration
+ matched from the HTTP protocol host name. Non matching configurations
+ will cause the request to be rejected. The attribute default value is
+ <code>true</code>, enabling the matching. (remm)
+ </add>
</changelog>
</subsection>
<subsection name="Jasper">
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 7c2be18e6f..a9fcc3cfed 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -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>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]