Enabling host verification in Netty

Published
Updated

By default, Netty version 4 and lower are configured with Host verification disabled. While this is helpful during debugging or developing an application, it can become a security issue if brought in production.

There are a number of steps involved in certificate verification, one of these is to compare the hostname of the client or remote server to the certificate they are presenting. Should this hostname verification be disabled, any valid certificate presented by the same issuer could be accepted as valid, even though that certificate isn’t intended to be used by that hostname. Ultimately this could lead to an attacker using a custom made, or stolen certificate and could potentially lead to a man in the middle attack.

As a result of this, many static-analysis tools such as WhiteSource flag this as a security finding when using the netty-all dependency. Additionally, Netty is a transitive dependency found in many other popular projects such as Spring, the AWS SDK, Apache Http Client, Gremlin, Spark, Cassandra, and many others.

Unfortunately, the Netty developers have decided not to alter this default configuration in Netty versions 4 and lower, but will enable host verification by default in the upcoming Netty 5 beta.

The following examples can be used as a reference to enable hostname verification within Netty, even if your application does not directly interface with it.

Enabling Hostname Verification using Netty’s SSLEngine

If your application has visibility or scope over Netty’s SSLEngine, the fix is as simple as getting the SSLParameters and configuring the Endpoint Identification:


SSLEngine sslEngine = getSSLEngine(); // Get your sslEngine reference

// The below code will reconfigure Netty's SSLEngine to enable hostname verification
SSLParameters sslParameters = sslEngine.getSSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
sslEngine.setSSLParameters(sslParameters);

Unfortunately, the SSLEngine is not typically visible or configurable except upon initial connection initialization which makes the above configuration change not possible without some additional code.

Configuring Netty’s SslContext

Most libraries that implement Netty will give you access to the SslContext to be used within the underlying library usually via a Builder or setter. Knowing this, we can implement a kind of decorator pattern to Netty’s default SslContext and return one that has a modified SSLEngine as described in the above section.

Note: don’t mix up Netty’s SslContext up with Java’s SSLContext as they are different and not interchangable


SslContext context = new SslContext() {
    // Alter the below context as needed, adding in any extra configurations needed for trust stores and such.
    // I have provided a JDK default implementation that should work in most situations
    SslContext sslContext = SslContextBuilder.forClient()
        .sslProvider(SslProvider.JDK)
        .keyManager(KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()))
        .trustManager(TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()))
        .build();
        
        @Override
        public boolean isClient() {
            return sslContext.isClient();
        }

        @Override
        public SSLSessionContext sessionContext() {
            return sslContext.sessionContext();
        }

        @Override
        public List<String> cipherSuites() {
            return sslContext.cipherSuites();
        }
        
        @Override
        public ApplicationProtocolNegotiator applicationProtocolNegotiator() {
            return sslContext.applicationProtocolNegotiator();
        }

        @Override
        public SSLEngine newEngine(ByteBufAllocator byteBufAllocator) {
            SSLEngine sslEngine = sslContext.newEngine(byteBufAllocator);
            // Hostname verification fix below:
            SSLParameters sslParameters = sslEngine.getSSLParameters();
            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
            sslEngine.setSSLParameters(sslParameters);
            return engine;
        }
        
        @Override
        public SSLEngine newEngine(ByteBufAllocator byteBufAllocator, String s, int i) {
            SSLEngine sslEngine = sslContext.newEngine(byteBufAllocator, s, i);
            // Hostname verification fix below:
            SSLParameters sslParameters = sslEngine.getSSLParameters();
            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
            sslEngine.setSSLParameters(sslParameters);
            return engine;
        }
};

Simply pass the reference to your context to the library and it should now use the modified configuration. For example, in Spring using a WebClient it would be something like the following:

@Bean
public WebClient webClient() throws Exception {
    HttpClient httpClient = HttpClient
        .create()
        .secure(t -> t.sslContext(context)); // Reference our context defined above
    return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();
}

When configuring Tinkerpop / Gremlin, you could do something like:

Cluster.Builder builder = Cluster.build();
builder.addContactPoint("someurlhere");
builder.port(1234);
builder.sslContext(context); // Reference our context defined above