Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Commit

Permalink
[PAN-2287] Added rebind mitigation for websockets. (#905)
Browse files Browse the repository at this point in the history
  • Loading branch information
mark-terry authored Feb 19, 2019
1 parent c925a10 commit c759554
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Optional;

public class PantheonFactoryConfigurationBuilder {
Expand Down Expand Up @@ -99,6 +100,7 @@ public PantheonFactoryConfigurationBuilder webSocketEnabled() {
final WebSocketConfiguration config = WebSocketConfiguration.createDefault();
config.setEnabled(true);
config.setPort(0);
config.setHostsWhitelist(Collections.singleton("*"));

this.webSocketConfiguration = config;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
Expand All @@ -37,6 +38,7 @@ public class WebSocketConfiguration {
private long refreshDelay;
private boolean authenticationEnabled = false;
private String authenticationCredentialsFile;
private Collection<String> hostsWhitelist = Collections.singletonList("localhost");

public static WebSocketConfiguration createDefault() {
final WebSocketConfiguration config = new WebSocketConfiguration();
Expand Down Expand Up @@ -142,4 +144,12 @@ public void setAuthenticationCredentialsFile(final String authenticationCredenti
public String getAuthenticationCredentialsFile() {
return authenticationCredentialsFile;
}

public void setHostsWhitelist(final Collection<String> hostsWhitelist) {
this.hostsWhitelist = hostsWhitelist;
}

public Collection<String> getHostsWhitelist() {
return Collections.unmodifiableCollection(this.hostsWhitelist);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
package tech.pegasys.pantheon.ethereum.jsonrpc.websocket;

import static com.google.common.collect.Streams.stream;

import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationService;
import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationUtils;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager;
Expand All @@ -21,6 +23,8 @@
import java.util.concurrent.CompletableFuture;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
Expand All @@ -30,6 +34,7 @@
import io.vertx.core.http.ServerWebSocket;
import io.vertx.core.net.SocketAddress;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -100,6 +105,10 @@ private Handler<ServerWebSocket> websocketHandler() {
LOG.trace("Websocket authentication token {}", token);
}

if (!hasWhitelistedHostnameHeader(Optional.ofNullable(websocket.headers().get("Host")))) {
websocket.reject(403);
}

LOG.debug("Websocket Connected ({})", socketAddressAsString(socketAddress));

websocket.handler(
Expand Down Expand Up @@ -138,6 +147,10 @@ private Handler<ServerWebSocket> websocketHandler() {

private Handler<HttpServerRequest> httpHandler() {
final Router router = Router.router(vertx);

// Verify Host header to avoid rebind attack.
router.route().handler(checkWhitelistHostHeader());

if (authenticationService.isPresent()) {
router.route("/login").handler(BodyHandler.create());
router
Expand Down Expand Up @@ -212,4 +225,47 @@ private String getAuthToken(final ServerWebSocket websocket) {
return AuthenticationUtils.getJwtTokenFromAuthorizationHeaderValue(
websocket.headers().get("Authorization"));
}

private Handler<RoutingContext> checkWhitelistHostHeader() {
return event -> {
if (hasWhitelistedHostnameHeader(Optional.ofNullable(event.request().host()))) {
event.next();
} else {
event
.response()
.setStatusCode(403)
.putHeader("Content-Type", "application/json; charset=utf-8")
.end("{\"message\":\"Host not authorized.\"}");
}
};
}

@VisibleForTesting
public boolean hasWhitelistedHostnameHeader(final Optional<String> header) {
return configuration.getHostsWhitelist().contains("*")
|| header.map(value -> checkHostInWhitelist(validateHostHeader(value))).orElse(false);
}

private Optional<String> validateHostHeader(final String header) {
final Iterable<String> splitHostHeader = Splitter.on(':').split(header);
final long hostPieces = stream(splitHostHeader).count();
if (hostPieces > 1) {
// If the host contains a colon, verify the host is correctly formed - host [ ":" port ]
if (hostPieces > 2 || !Iterables.get(splitHostHeader, 1).matches("\\d{1,5}+")) {
return Optional.empty();
}
}
return Optional.ofNullable(Iterables.get(splitHostHeader, 0));
}

private boolean checkHostInWhitelist(final Optional<String> hostHeader) {
return hostHeader
.map(
header ->
configuration.getHostsWhitelist().stream()
.anyMatch(
whitelistEntry ->
whitelistEntry.toLowerCase().equals(header.toLowerCase())))
.orElse(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright 2018 ConsenSys AG.
*
* 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 tech.pegasys.pantheon.ethereum.jsonrpc.websocket;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;

import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.methods.WebSocketMethodsFactory;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;

@RunWith(VertxUnitRunner.class)
public class WebSocketHostWhitelistTest {

@ClassRule public static final TemporaryFolder folder = new TemporaryFolder();

protected static Vertx vertx;

private final List<String> hostsWhitelist = Arrays.asList("ally", "friend");

private final WebSocketConfiguration webSocketConfiguration =
WebSocketConfiguration.createDefault();
private static WebSocketRequestHandler webSocketRequestHandlerSpy;
private WebSocketService websocketService;
private HttpClient httpClient;
private static final int VERTX_AWAIT_TIMEOUT_MILLIS = 10000;

@Before
public void initServerAndClient() {
vertx = Vertx.vertx();

final Map<String, JsonRpcMethod> websocketMethods =
new WebSocketMethodsFactory(new SubscriptionManager(), new HashMap<>()).methods();
webSocketRequestHandlerSpy = spy(new WebSocketRequestHandler(vertx, websocketMethods));

websocketService =
new WebSocketService(vertx, webSocketConfiguration, webSocketRequestHandlerSpy);
websocketService.start().join();

final HttpClientOptions httpClientOptions =
new HttpClientOptions()
.setDefaultHost(webSocketConfiguration.getHost())
.setDefaultPort(webSocketConfiguration.getPort());

httpClient = vertx.createHttpClient(httpClientOptions);
}

@After
public void after() {
reset(webSocketRequestHandlerSpy);
websocketService.stop();
}

@Test
public void websocketRequestWithDefaultHeaderAndDefaultConfigIsAccepted() {
boolean result = websocketService.hasWhitelistedHostnameHeader(Optional.of("localhost:50012"));
assertThat(result).isTrue();
}

@Test
public void httpRequestWithDefaultHeaderAndDefaultConfigIsAccepted(final TestContext context) {
doHttpRequestAndVerify(context, "localhost:50012", 400);
}

@Test
public void websocketRequestWithEmptyHeaderAndDefaultConfigIsRejected() {
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of(""))).isFalse();
}

@Test
public void httpRequestWithEmptyHeaderAndDefaultConfigIsRejected(final TestContext context) {
doHttpRequestAndVerify(context, "", 403);
}

@Test
public void websocketRequestWithAnyHostnameAndWildcardConfigIsAccepted() {
webSocketConfiguration.setHostsWhitelist(Collections.singletonList("*"));
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of("ally"))).isTrue();
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of("foe"))).isTrue();
}

@Test
public void httpRequestWithAnyHostnameAndWildcardConfigIsAccepted(final TestContext context) {
webSocketConfiguration.setHostsWhitelist(Collections.singletonList("*"));
doHttpRequestAndVerify(context, "ally", 400);
doHttpRequestAndVerify(context, "foe", 400);
}

@Test
public void websocketRequestWithWhitelistedHostIsAccepted() {
webSocketConfiguration.setHostsWhitelist(hostsWhitelist);
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of("ally"))).isTrue();
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of("ally:12345"))).isTrue();
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of("friend"))).isTrue();
}

@Test
public void httpRequestWithWhitelistedHostIsAccepted(final TestContext context) {
webSocketConfiguration.setHostsWhitelist(hostsWhitelist);
doHttpRequestAndVerify(context, "ally", 400);
doHttpRequestAndVerify(context, "ally:12345", 400);
doHttpRequestAndVerify(context, "friend", 400);
}

@Test
public void websocketRequestWithUnknownHostIsRejected() {
webSocketConfiguration.setHostsWhitelist(hostsWhitelist);
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of("foe"))).isFalse();
}

@Test
public void httpRequestWithUnknownHostIsRejected(final TestContext context) {
webSocketConfiguration.setHostsWhitelist(hostsWhitelist);
doHttpRequestAndVerify(context, "foe", 403);
}

@Test
public void websocketRequestWithMalformedHostIsRejected() {
webSocketConfiguration.setAuthenticationEnabled(false);
webSocketConfiguration.setHostsWhitelist(hostsWhitelist);
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of("ally:friend"))).isFalse();
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of("ally:123456"))).isFalse();
assertThat(websocketService.hasWhitelistedHostnameHeader(Optional.of("ally:friend:1234")))
.isFalse();
}

@Test
public void httpRequestWithMalformedHostIsRejected(final TestContext context) {
webSocketConfiguration.setAuthenticationEnabled(false);
webSocketConfiguration.setHostsWhitelist(hostsWhitelist);
doHttpRequestAndVerify(context, "ally:friend", 403);
doHttpRequestAndVerify(context, "ally:123456", 403);
doHttpRequestAndVerify(context, "ally:friend:1234", 403);
}

private void doHttpRequestAndVerify(
final TestContext context, final String hostname, final int expectedResponse) {
final Async async = context.async();

final HttpClientRequest request =
httpClient.post(
webSocketConfiguration.getPort(),
webSocketConfiguration.getHost(),
"/",
response -> {
assertThat(response.statusCode()).isEqualTo(expectedResponse);
async.complete();
});

request.putHeader("Host", hostname);
request.end();

async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -70,6 +71,7 @@ public void before() throws URISyntaxException {
websocketConfiguration.setPort(0);
websocketConfiguration.setAuthenticationEnabled(true);
websocketConfiguration.setAuthenticationCredentialsFile(authTomlPath);
websocketConfiguration.setHostsWhitelist(Collections.singleton("*"));

final Map<String, JsonRpcMethod> websocketMethods =
new WebSocketMethodsFactory(new SubscriptionManager(), new HashMap<>()).methods();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -56,6 +57,7 @@ public void before() {

websocketConfiguration = WebSocketConfiguration.createDefault();
websocketConfiguration.setPort(0);
websocketConfiguration.setHostsWhitelist(Collections.singleton("*"));

final Map<String, JsonRpcMethod> websocketMethods =
new WebSocketMethodsFactory(new SubscriptionManager(), new HashMap<>()).methods();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,7 @@ private WebSocketConfiguration webSocketConfiguration() {
webSocketConfiguration.setRefreshDelay(rpcWsRefreshDelay);
webSocketConfiguration.setAuthenticationEnabled(isRpcWsAuthenticationEnabled);
webSocketConfiguration.setAuthenticationCredentialsFile(rpcWsAuthenticationCredentialsFile());
webSocketConfiguration.setHostsWhitelist(hostsWhitelist);
return webSocketConfiguration;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ private WebSocketConfiguration wsRpcConfiguration() {
final WebSocketConfiguration configuration = WebSocketConfiguration.createDefault();
configuration.setPort(0);
configuration.setEnabled(true);
configuration.setHostsWhitelist(Collections.singletonList("*"));
return configuration;
}

Expand Down

0 comments on commit c759554

Please sign in to comment.