Host header attack implementation improvements and tests

Signed-off-by: Ammar Bikic <ammar.bikic@bosch.io>
This commit is contained in:
Ammar Bikic
2020-12-04 13:33:59 +01:00
parent e23f4dae63
commit 98f7a5b9f3
5 changed files with 70 additions and 15 deletions

View File

@@ -20,6 +20,7 @@ import javax.servlet.FilterConfig;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.hawkbit.cache.DownloadIdCache; import org.eclipse.hawkbit.cache.DownloadIdCache;
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
@@ -92,6 +93,7 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter; import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall; import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.session.HttpSessionEventPublisher;
@@ -725,17 +727,39 @@ public class SecurityManagedConfiguration {
@Bean @Bean
public HttpFirewall httpFirewall() { public HttpFirewall httpFirewall() {
final List<String> allowedHostNames = hawkbitSecurityProperties.getAllowedHostNames(); final List<String> allowedHostNames = hawkbitSecurityProperties.getAllowedHostNames();
final StrictHttpFirewall firewall = new StrictHttpFirewall(); final IgnorePathsStrictHttpFirewall firewall = new IgnorePathsStrictHttpFirewall(
hawkbitSecurityProperties.getHttpFirewallIgnoredPaths());
if (allowedHostNames != null && !CollectionUtils.isEmpty(allowedHostNames)) { if (!CollectionUtils.isEmpty(allowedHostNames)) {
firewall.setAllowedHostnames(hostName -> { firewall.setAllowedHostnames(hostName -> {
LOG.info("Firewall check host: {}, allowed: {}", hostName, allowedHostNames.contains(hostName)); LOG.debug("Firewall check host: {}, allowed: {}", hostName, allowedHostNames.contains(hostName));
return allowedHostNames.stream() return allowedHostNames.contains(hostName);});
.anyMatch(allowedHostName -> allowedHostName.equals(hostName));});
} }
return firewall; return firewall;
} }
private static class IgnorePathsStrictHttpFirewall extends StrictHttpFirewall {
private final Collection<String> pathsToIgnore;
public IgnorePathsStrictHttpFirewall(final Collection<String> pathsToIgnore) {
super();
this.pathsToIgnore = pathsToIgnore;
}
@Override
public FirewalledRequest getFirewalledRequest(final HttpServletRequest request) {
if (pathsToIgnore != null && pathsToIgnore.contains(request.getRequestURI())) {
return new FirewalledRequest(request) {
@Override
public void reset() {
}
};
}
return super.getFirewalledRequest(request);
}
}
@Override @Override
public void configure(final WebSecurity webSecurity) throws Exception { public void configure(final WebSecurity webSecurity) throws Exception {
// No security for static content // No security for static content

View File

@@ -1,3 +1,11 @@
/**
* Copyright (c) 2020 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.app; package org.eclipse.hawkbit.app;
import org.eclipse.hawkbit.repository.test.util.MsSqlTestDatabase; import org.eclipse.hawkbit.repository.test.util.MsSqlTestDatabase;

View File

@@ -8,33 +8,38 @@
*/ */
package org.eclipse.hawkbit.app; package org.eclipse.hawkbit.app;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.security.web.firewall.RequestRejectedException; import org.springframework.security.web.firewall.RequestRejectedException;
import io.qameta.allure.Feature; import io.qameta.allure.Feature;
import io.qameta.allure.Story; import io.qameta.allure.Story;
import org.springframework.test.context.TestPropertySource;
@SpringBootTest(properties = { "hawkbit.server.security.allowedHostNames=localhost" }) @TestPropertySource(properties = { "hawkbit.server.security.allowedHostNames=localhost",
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html" })
@Feature("Integration Test - Security") @Feature("Integration Test - Security")
@Story("Allowed Host Names") @Story("Allowed Host Names")
public class AllowedHostNamesTest extends AbstractSecurityTest { public class AllowedHostNamesTest extends AbstractSecurityTest {
@Test @Test
public void allowedHostNameWithNotAllowedHost() throws Exception { public void allowedHostNameWithNotAllowedHost() {
try { assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(
mvc.perform(get("/").header(HttpHeaders.HOST, "www.google.com")); () -> mvc.perform(get("/").header(HttpHeaders.HOST, "www.google.com")));
} catch (final RequestRejectedException e) {
// do nothing as this exception is expected
}
} }
@Test @Test
public void allowedHostNameWithAllowedHost() throws Exception { public void allowedHostNameWithAllowedHost() throws Exception {
mvc.perform(get("/").header(HttpHeaders.HOST, "localhost")).andExpect(status().is3xxRedirection()); mvc.perform(get("/").header(HttpHeaders.HOST, "localhost")).andExpect(status().is3xxRedirection());
} }
}
@Test
public void notAllowedHostnameWithIgnoredPath() throws Exception {
mvc.perform(get("/index.html").header(HttpHeaders.HOST, "www.google.com"))
.andExpect(status().is4xxClientError());
}
}

View File

@@ -24,6 +24,7 @@ import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
@@ -36,7 +37,7 @@ import io.qameta.allure.Description;
import io.qameta.allure.Feature; import io.qameta.allure.Feature;
import io.qameta.allure.Story; import io.qameta.allure.Story;
@SpringBootTest(properties = { "hawkbit.server.security.cors.enabled=true", @TestPropertySource(properties = { "hawkbit.server.security.cors.enabled=true",
"hawkbit.server.security.cors.allowedOrigins=" + CorsTest.ALLOWED_ORIGIN_FIRST + "," "hawkbit.server.security.cors.allowedOrigins=" + CorsTest.ALLOWED_ORIGIN_FIRST + ","
+ CorsTest.ALLOWED_ORIGIN_SECOND }) + CorsTest.ALLOWED_ORIGIN_SECOND })
@Feature("Integration Test - Security") @Feature("Integration Test - Security")

View File

@@ -35,8 +35,17 @@ public class HawkbitSecurityProperties {
*/ */
private boolean requireSsl; private boolean requireSsl;
/**
* With this property a list of allowed hostnames can be configured. All
* requests with different Host headers will be rejected.
*/
private List<String> allowedHostNames; private List<String> allowedHostNames;
/**
* Add paths that will be ignored by {@link StrictHttpFirewall}.
*/
private List<String> httpFirewallIgnoredPaths;
/** /**
* Basic authentication realm, see * Basic authentication realm, see
* https://tools.ietf.org/html/rfc2617#page-3 . * https://tools.ietf.org/html/rfc2617#page-3 .
@@ -59,6 +68,14 @@ public class HawkbitSecurityProperties {
this.allowedHostNames = allowedHostNames; this.allowedHostNames = allowedHostNames;
} }
public List<String> getHttpFirewallIgnoredPaths() {
return httpFirewallIgnoredPaths;
}
public void setHttpFirewallIgnoredPaths(final List<String> httpFirewallIgnoredPaths) {
this.httpFirewallIgnoredPaths = httpFirewallIgnoredPaths;
}
public String getBasicRealm() { public String getBasicRealm() {
return basicRealm; return basicRealm;
} }