Skip to content

Commit

Permalink
KREST-7955 421 misdirected request if host does not match SNI (#410)
Browse files Browse the repository at this point in the history
* SniHandler for checking SNI against host name

* Add config sni.check.enabled to control the SniHandler feature

* Integration tests for SniHandler

* Make tests also parameterized on http2 enabled
  • Loading branch information
trnguyencflt committed Sep 11, 2023
1 parent 4cb554e commit c721242
Show file tree
Hide file tree
Showing 4 changed files with 406 additions and 0 deletions.
5 changes: 5 additions & 0 deletions core/src/main/java/io/confluent/rest/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import io.confluent.rest.exceptions.JsonParseExceptionMapper;
import io.confluent.rest.extension.ResourceExtension;
import io.confluent.rest.filters.CsrfTokenProtectionFilter;
import io.confluent.rest.handlers.SniHandler;
import io.confluent.rest.metrics.Jetty429MetricsDosFilterListener;
import io.confluent.rest.metrics.JettyRequestMetricsFilter;
import io.confluent.rest.metrics.MetricsResourceMethodApplicationListener;
Expand Down Expand Up @@ -412,6 +413,10 @@ public Handler configureHandler() {
requestLogHandler.setRequestLog(requestLog);
context.insertHandler(requestLogHandler);

if (config.getSniCheckEnable()) {
context.insertHandler(new SniHandler());
}

HandlerCollection handlers = new HandlerCollection();
handlers.setHandlers(new Handler[]{context});

Expand Down
16 changes: 16 additions & 0 deletions core/src/main/java/io/confluent/rest/RestConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,12 @@ public class RestConfig extends AbstractConfig {
+ "Java 11 JVM or later. Default is true.";
protected static final boolean HTTP2_ENABLED_DEFAULT = true;

public static final String SNI_CHECK_ENABLED_CONFIG = "sni.check.enabled";
protected static final String SNI_CHECK_ENABLED_DOC =
"Whether or not to check the SNI against the Host header. If the values don't match, "
+ "returns a 421 misdirected response. Default is false.";
protected static final boolean SNI_CHECK_ENABLED_DEFAULT = false;

public static final String PROXY_PROTOCOL_ENABLED_CONFIG =
"proxy.protocol.enabled";
protected static final String PROXY_PROTOCOL_ENABLED_DOC =
Expand Down Expand Up @@ -1053,6 +1059,12 @@ private static ConfigDef incompleteBaseConfigDef() {
HTTP2_ENABLED_DEFAULT,
Importance.LOW,
HTTP2_ENABLED_DOC
).define(
SNI_CHECK_ENABLED_CONFIG,
Type.BOOLEAN,
SNI_CHECK_ENABLED_DEFAULT,
Importance.LOW,
SNI_CHECK_ENABLED_DOC
).define(
LISTENER_PROTOCOL_MAP_CONFIG,
Type.LIST,
Expand Down Expand Up @@ -1391,6 +1403,10 @@ public final int getNetworkTrafficRateLimitBytesPerSec() {
return getInt(NETWORK_TRAFFIC_RATE_LIMIT_BYTES_PER_SEC_CONFIG);
}

public final boolean getSniCheckEnable() {
return getBoolean(SNI_CHECK_ENABLED_CONFIG);
}

/**
* <p>A helper method for extracting multi-instance application configuration,
* specified via property names of the form PREFIX[.LISTENER_NAME].PROPERTY.</p>
Expand Down
75 changes: 75 additions & 0 deletions core/src/main/java/io/confluent/rest/handlers/SniHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2014 - 2023 Confluent Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.confluent.rest.handlers;

import static org.eclipse.jetty.http.HttpStatus.Code.MISDIRECTED_REQUEST;

import java.io.IOException;
import java.util.List;
import javax.net.ssl.ExtendedSSLSession;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLSession;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ssl.SslConnection.DecryptedEndPoint;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SniHandler extends HandlerWrapper {
private static final Logger log = LoggerFactory.getLogger(SniHandler.class);

@Override
public void handle(String target, Request baseRequest,
HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
String serverName = request.getServerName();
String sniServerName = getSniServerName(baseRequest);
if (sniServerName != null && !sniServerName.equals(serverName)) {
log.debug("Sni check failed, host header: {}, sni value: {}", serverName, sniServerName);
baseRequest.setHandled(true);
response.sendError(MISDIRECTED_REQUEST.getCode(), MISDIRECTED_REQUEST.getMessage());
}
super.handle(target, baseRequest, request, response);
}

private static String getSniServerName(Request baseRequest) {
EndPoint endpoint = baseRequest.getHttpChannel().getEndPoint();
if (endpoint instanceof DecryptedEndPoint) {
SSLSession session = ((DecryptedEndPoint) endpoint)
.getSslConnection()
.getSSLEngine()
.getSession();
if (session instanceof ExtendedSSLSession) {
List<SNIServerName> servers = ((ExtendedSSLSession) session).getRequestedServerNames();
if (servers != null) {
return servers.stream()
.findAny()
.filter(SNIHostName.class::isInstance)
.map(SNIHostName.class::cast)
.map(SNIHostName::getAsciiName)
.orElse(null);
}
}
}
return null;
}
}
Loading

0 comments on commit c721242

Please sign in to comment.