Skip to content

Commit

Permalink
Configurable HSTS in server
Browse files Browse the repository at this point in the history
The HTTP Strict-Transport-Security (HSTS) response header tells
browsers that a webpage should only be accessed using HTTPS instead
of HTTP. Webpage is thus less vulnerable to man-in-the-middle attacks.

This commit adds a setting to configure value of the
`Strict-Transport-Security` response header for neo4j server. Setting
can be used to make neo4j browser endpoint send the HSTS header with
specified max-age, when accessed through HTTPS.

See
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
for more details.
  • Loading branch information
lutovich committed May 16, 2018
1 parent b3bf529 commit cd2a378
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 28 deletions.
Expand Up @@ -367,7 +367,7 @@ public static Config defaults( @Nonnull final Map<String,String> initialSettings
* @param value The initial value to give the setting.
*/
@Nonnull
public static Config defaults( @Nonnull final Setting<?> setting, @Nonnull final String value )
public static Config defaults( @Nonnull final Setting<?> setting, final String value )
{
return builder().withSetting( setting, value ).build();
}
Expand Down
Expand Up @@ -186,6 +186,12 @@ private ThirdPartyJaxRsPackage createThirdPartyJaxRsPackage( String packageAndMo
public static final Setting<Duration> transaction_idle_timeout = setting( "dbms.rest.transaction.idle_timeout",
DURATION, "60s" );

@Description( "Value of the HTTP Strict-Transport-Security (HSTS) response header. " +
"This header tells browsers that a webpage should only be accessed using HTTPS instead of HTTP. It is attached to every HTTPS response. " +
"Setting is not set by default so 'Strict-Transport-Security' header is not sent. " +
"Value is expected to contain dirictives like 'max-age', 'includeSubDomains' and 'preload'." )
public static final Setting<String> http_strict_transport_security = setting( "dbms.security.http_strict_transport_security", STRING, NO_DEFAULT );

@SuppressWarnings( "unused" ) // accessed from the browser
@Description( "Commands to be run when Neo4j Browser successfully connects to this server. Separate multiple " +
"commands with semi-colon." )
Expand Down
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2002-2018 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.server.security.ssl;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;

import org.neo4j.kernel.configuration.Config;
import org.neo4j.server.configuration.ServerSettings;

import static org.eclipse.jetty.http.HttpHeader.STRICT_TRANSPORT_SECURITY;

public class HttpsRequestCustomizer implements HttpConfiguration.Customizer
{
private final HttpField hstsResponseField;

public HttpsRequestCustomizer( Config config )
{
hstsResponseField = createHstsResponseField( config );
}

@Override
public void customize( Connector connector, HttpConfiguration channelConfig, Request request )
{
request.setScheme( HttpScheme.HTTPS.asString() );

if ( hstsResponseField != null )
{
request.getResponse().getHttpFields().add( hstsResponseField );
}
}

private static HttpField createHstsResponseField( Config config )
{
String configuredValue = config.get( ServerSettings.http_strict_transport_security );
if ( StringUtils.isBlank( configuredValue ) )
{
return null;
}
return new PreEncodedHttpField( STRICT_TRANSPORT_SECURITY, configuredValue );
}
}
Expand Up @@ -19,9 +19,9 @@
*/
package org.neo4j.server.security.ssl;

import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConfiguration.Customizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
Expand All @@ -39,17 +39,19 @@

public class SslSocketConnectorFactory extends HttpConnectorFactory
{
public SslSocketConnectorFactory( Config configuration )
private final Customizer requestCustomizer;

public SslSocketConnectorFactory( Config config )
{
super( configuration );
super( config );
requestCustomizer = new HttpsRequestCustomizer( config );
}

@Override
protected HttpConfiguration createHttpConfig()
{
HttpConfiguration httpConfig = super.createHttpConfig();
httpConfig.addCustomizer(
( connector, channelConfig, request ) -> request.setScheme( HttpScheme.HTTPS.asString() ) );
httpConfig.addCustomizer( requestCustomizer );
return httpConfig;
}

Expand Down
123 changes: 101 additions & 22 deletions community/server/src/test/java/org/neo4j/server/HttpHeadersIT.java
Expand Up @@ -25,8 +25,8 @@
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.client.urlconnection.HTTPSProperties;
import org.eclipse.jetty.http.HttpHeader;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.net.URI;
Expand All @@ -37,11 +37,15 @@
import javax.net.ssl.TrustManager;
import javax.ws.rs.core.MultivaluedMap;

import org.neo4j.server.configuration.ServerSettings;
import org.neo4j.server.helpers.CommunityServerBuilder;
import org.neo4j.test.server.ExclusiveServerTestBase;
import org.neo4j.test.server.InsecureTrustManager;

import static com.sun.jersey.client.urlconnection.HTTPSProperties.PROPERTY_HTTPS_PROPERTIES;
import static java.util.Collections.emptyList;
import static org.eclipse.jetty.http.HttpHeader.SERVER;
import static org.eclipse.jetty.http.HttpHeader.STRICT_TRANSPORT_SECURITY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
Expand All @@ -51,16 +55,6 @@ public class HttpHeadersIT extends ExclusiveServerTestBase
{
private CommunityNeoServer server;

@Before
public void setUp() throws Exception
{
server = serverOnRandomPorts().withHttpsEnabled()
.usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
.build();

server.start();
}

@After
public void tearDown() throws Exception
{
Expand All @@ -73,34 +67,119 @@ public void tearDown() throws Exception
@Test
public void shouldNotSendJettyVersionWithHttpResponseHeaders() throws Exception
{
URI httpUri = server.baseUri();
testNoJettyVersionInResponseHeaders( httpUri );
startServer();
testNoJettyVersionInResponseHeaders( httpUri() );
}

@Test
public void shouldNotSendJettyVersionWithHttpsResponseHeaders() throws Exception
{
URI httpsUri = server.httpsUri().orElseThrow( IllegalStateException::new );
testNoJettyVersionInResponseHeaders( httpsUri );
startServer();
testNoJettyVersionInResponseHeaders( httpsUri() );
}

private static void testNoJettyVersionInResponseHeaders( URI baseUri ) throws Exception
@Test
public void shouldNotSendHstsHeaderWithHttpResponse() throws Exception
{
URI uri = baseUri.resolve( "db/data/transaction/commit" );
ClientRequest request = createClientRequest( uri );
startServer( "max-age=3600" );
assertNull( runRequestAndGetHstsHeaderValue( httpUri() ) );
}

ClientResponse response = createClient().handle( request );
@Test
public void shouldSendHstsHeaderWithHttpsResponse() throws Exception
{
String hstsValue = "max-age=31536000; includeSubDomains";
startServer( hstsValue );
assertEquals( hstsValue, runRequestAndGetHstsHeaderValue( httpsUri() ) );
}

assertEquals( 200, response.getStatus() );
@Test
public void shouldNotSendHstsHeaderWithHttpsResponseWhenNotConfigured() throws Exception
{
startServer();
assertNull( runRequestAndGetHstsHeaderValue( httpsUri() ) );
}

private void startServer() throws Exception
{
startServer( null );
}

private void startServer( String hstsValue ) throws Exception
{
server = buildServer( hstsValue );
server.start();
}

private CommunityNeoServer buildServer( String hstsValue ) throws Exception
{
CommunityServerBuilder builder = serverOnRandomPorts()
.withHttpsEnabled()
.usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() );

if ( hstsValue != null )
{
builder.withProperty( ServerSettings.http_strict_transport_security.name(), hstsValue );
}

return builder.build();
}

private URI httpUri()
{
return server.baseUri();
}

private URI httpsUri()
{
return server.httpsUri().orElseThrow( IllegalStateException::new );
}

private static void testNoJettyVersionInResponseHeaders( URI baseUri ) throws Exception
{
MultivaluedMap<String,String> headers = runRequestAndGetHeaders( baseUri );

assertNull( headers.get( SERVER.asString() ) ); // no 'Server' header

MultivaluedMap<String,String> headers = response.getHeaders();
assertNull( headers.get( SERVER.name() ) ); // no 'Server' header
for ( List<String> values : headers.values() )
{
assertFalse( values.stream().anyMatch( value -> value.toLowerCase().contains( "jetty" ) ) ); // no 'jetty' in other header values
}
}

private static String runRequestAndGetHstsHeaderValue( URI baseUri ) throws Exception
{
List<String> values = runRequestAndGetHeaderValues( baseUri, STRICT_TRANSPORT_SECURITY );
if ( values.isEmpty() )
{
return null;
}
else if ( values.size() == 1 )
{
return values.get( 0 );
}
else
{
throw new IllegalStateException( "Unexpected number of " + STRICT_TRANSPORT_SECURITY.asString() + " header values: " + values );
}
}

private static List<String> runRequestAndGetHeaderValues( URI baseUri, HttpHeader header ) throws Exception
{
return runRequestAndGetHeaders( baseUri ).getOrDefault( header.asString(), emptyList() );
}

private static MultivaluedMap<String,String> runRequestAndGetHeaders( URI baseUri ) throws Exception
{
URI uri = baseUri.resolve( "db/data/transaction/commit" );
ClientRequest request = createClientRequest( uri );

ClientResponse response = createClient().handle( request );
assertEquals( 200, response.getStatus() );

return response.getHeaders();
}

private static ClientRequest createClientRequest( URI uri )
{
return ClientRequest.create()
Expand Down

0 comments on commit cd2a378

Please sign in to comment.