Host header attack implementation improvements and tests
Signed-off-by: Ammar Bikic <ammar.bikic@bosch.io>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user