diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index e08b1eae97..62ccc42043 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -49,8 +49,9 @@ import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.debug.DebugFilter; -import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.RequestRejectedHandler; +import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -91,6 +92,8 @@ public final class WebSecurity extends private HttpFirewall httpFirewall; + private RequestRejectedHandler requestRejectedHandler; + private boolean debugEnabled; private WebInvocationPrivilegeEvaluator privilegeEvaluator; @@ -295,6 +298,9 @@ public final class WebSecurity extends if (httpFirewall != null) { filterChainProxy.setFirewall(httpFirewall); } + if (requestRejectedHandler != null) { + filterChainProxy.setRequestRejectedHandler(requestRejectedHandler); + } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; @@ -392,5 +398,8 @@ public final class WebSecurity extends try { this.httpFirewall = applicationContext.getBean(HttpFirewall.class); } catch(NoSuchBeanDefinitionException e) {} + try { + this.requestRejectedHandler = applicationContext.getBean(RequestRejectedHandler.class); + } catch(NoSuchBeanDefinitionException e) {} } } diff --git a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java index d0d348b751..37019e3204 100644 --- a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java +++ b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java @@ -19,11 +19,15 @@ package org.springframework.security.web; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.firewall.DefaultRequestRejectedHandler; import org.springframework.security.web.firewall.FirewalledRequest; import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.RequestRejectedException; +import org.springframework.security.web.firewall.RequestRejectedHandler; import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.UrlUtils; +import org.springframework.util.Assert; import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.filter.GenericFilterBean; @@ -149,6 +153,8 @@ public class FilterChainProxy extends GenericFilterBean { private HttpFirewall firewall = new StrictHttpFirewall(); + private RequestRejectedHandler requestRejectedHandler = new DefaultRequestRejectedHandler(); + // ~ Methods // ======================================================================================================== @@ -176,6 +182,8 @@ public class FilterChainProxy extends GenericFilterBean { try { request.setAttribute(FILTER_APPLIED, Boolean.TRUE); doFilterInternal(request, response, chain); + } catch (RequestRejectedException e) { + requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, e); } finally { SecurityContextHolder.clearContext(); @@ -272,6 +280,17 @@ public class FilterChainProxy extends GenericFilterBean { this.firewall = firewall; } + /** + * Sets the {@link RequestRejectedHandler} to be used for requests rejected by the firewall. + * + * @since 5.2 + * @param requestRejectedHandler the {@link RequestRejectedHandler} + */ + public void setRequestRejectedHandler(RequestRejectedHandler requestRejectedHandler) { + Assert.notNull(requestRejectedHandler, "requestRejectedHandler may not be null"); + this.requestRejectedHandler = requestRejectedHandler; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/web/src/main/java/org/springframework/security/web/firewall/DefaultRequestRejectedHandler.java b/web/src/main/java/org/springframework/security/web/firewall/DefaultRequestRejectedHandler.java new file mode 100644 index 0000000000..ef63e9f7d0 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/DefaultRequestRejectedHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * 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 + * + * https://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 org.springframework.security.web.firewall; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Default implementation of {@link RequestRejectedHandler} that simply rethrows the exception. + * + * @author Leonard Brünings + * @since 5.2 + */ +public class DefaultRequestRejectedHandler implements RequestRejectedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + RequestRejectedException requestRejectedException) throws IOException, ServletException { + throw requestRejectedException; + } +} diff --git a/web/src/main/java/org/springframework/security/web/firewall/HttpStatusRequestRejectedHandler.java b/web/src/main/java/org/springframework/security/web/firewall/HttpStatusRequestRejectedHandler.java new file mode 100644 index 0000000000..06c6ba5eaf --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/HttpStatusRequestRejectedHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * 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 + * + * https://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 org.springframework.security.web.firewall; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A simple implementation of {@link RequestRejectedHandler} that sends an error with configurable status code. + * + * @author Leonard Brünings + * @since 5.2 + */ +public class HttpStatusRequestRejectedHandler implements RequestRejectedHandler { + private static final Log logger = LogFactory.getLog(HttpStatusRequestRejectedHandler.class); + + private final int httpError; + + /** + * Constructs an instance which uses {@code 400} as response code. + */ + public HttpStatusRequestRejectedHandler() { + httpError = HttpServletResponse.SC_BAD_REQUEST; + } + + /** + * Constructs an instance which uses a configurable http code as response. + * @param httpError http status code to use + */ + public HttpStatusRequestRejectedHandler(int httpError) { + this.httpError = httpError; + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + RequestRejectedException requestRejectedException) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Rejecting request due to: " + requestRejectedException.getMessage(), + requestRejectedException); + } + response.sendError(httpError); + } +} diff --git a/web/src/main/java/org/springframework/security/web/firewall/RequestRejectedHandler.java b/web/src/main/java/org/springframework/security/web/firewall/RequestRejectedHandler.java new file mode 100644 index 0000000000..f26afa5398 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/RequestRejectedHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * 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 + * + * https://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 org.springframework.security.web.firewall; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Used by {@link org.springframework.security.web.FilterChainProxy} to handle an + * RequestRejectedException. + * + * @author Leonard Brünings + * @since 5.2 + */ +public interface RequestRejectedHandler { + // ~ Methods + // ======================================================================================================== + + /** + * Handles an request rejected failure. + * + * @param request that resulted in an RequestRejectedException + * @param response so that the user agent can be advised of the failure + * @param requestRejectedException that caused the invocation + * + * @throws IOException in the event of an IOException + * @throws ServletException in the event of a ServletException + */ + void handle(HttpServletRequest request, HttpServletResponse response, + RequestRejectedException requestRejectedException) throws IOException, + ServletException; +} diff --git a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java index c16afe6a21..10067d6fd8 100644 --- a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java +++ b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java @@ -28,6 +28,8 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.firewall.FirewalledRequest; import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.RequestRejectedException; +import org.springframework.security.web.firewall.RequestRejectedHandler; import org.springframework.security.web.util.matcher.RequestMatcher; import javax.servlet.Filter; @@ -243,4 +245,21 @@ public class FilterChainProxyTests { any(HttpServletResponse.class)); assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); } + + @Test(expected = IllegalArgumentException.class) + public void setRequestRejectedHandlerDoesNotAcceptNull() { + fcp.setRequestRejectedHandler(null); + } + + @Test + public void requestRejectedHandlerIsCalledIfFirewallThrowsRequestRejectedException() throws Exception { + HttpFirewall fw = mock(HttpFirewall.class); + RequestRejectedHandler rjh = mock(RequestRejectedHandler.class); + fcp.setFirewall(fw); + fcp.setRequestRejectedHandler(rjh); + RequestRejectedException requestRejectedException = new RequestRejectedException("Contains illegal chars"); + when(fw.getFirewalledRequest(request)).thenThrow(requestRejectedException); + fcp.doFilter(request, response, chain); + verify(rjh).handle(eq(request), eq(response), eq((requestRejectedException))); + } } diff --git a/web/src/test/java/org/springframework/security/web/firewall/DefaultRequestRejectedHandlerTest.java b/web/src/test/java/org/springframework/security/web/firewall/DefaultRequestRejectedHandlerTest.java new file mode 100644 index 0000000000..ce245637d9 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/firewall/DefaultRequestRejectedHandlerTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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 + * + * https://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 org.springframework.security.web.firewall; + + +import static org.mockito.Mockito.mock; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.Test; + +public class DefaultRequestRejectedHandlerTest { + + @Test + public void defaultRequestRejectedHandlerRethrowsTheException() throws Exception { + // given: + RequestRejectedException requestRejectedException = new RequestRejectedException("rejected"); + DefaultRequestRejectedHandler sut = new DefaultRequestRejectedHandler(); + + //when: + try { + sut.handle(mock(HttpServletRequest.class), mock(HttpServletResponse.class), requestRejectedException); + } catch (RequestRejectedException exception) { + //then: + Assert.assertThat(exception.getMessage(), CoreMatchers.is("rejected")); + return; + } + Assert.fail("Exception was not rethrown"); + } +} diff --git a/web/src/test/java/org/springframework/security/web/firewall/HttpStatusRequestRejectedHandlerTest.java b/web/src/test/java/org/springframework/security/web/firewall/HttpStatusRequestRejectedHandlerTest.java new file mode 100644 index 0000000000..be6f9454ec --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/firewall/HttpStatusRequestRejectedHandlerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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 + * + * https://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 org.springframework.security.web.firewall; + + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; + +public class HttpStatusRequestRejectedHandlerTest { + + @Test + public void httpStatusRequestRejectedHandlerUsesStatus400byDefault() throws Exception { + //given: + HttpStatusRequestRejectedHandler sut = new HttpStatusRequestRejectedHandler(); + HttpServletResponse response = mock(HttpServletResponse.class); + + //when: + sut.handle(mock(HttpServletRequest.class), response, mock(RequestRejectedException.class)); + + // then: + verify(response).sendError(400); + } + + @Test + public void httpStatusRequestRejectedHandlerCanBeConfiguredToUseStatus() throws Exception { + httpStatusRequestRejectedHandlerCanBeConfiguredToUseStatusHelper(400); + httpStatusRequestRejectedHandlerCanBeConfiguredToUseStatusHelper(403); + httpStatusRequestRejectedHandlerCanBeConfiguredToUseStatusHelper(500); + } + + private void httpStatusRequestRejectedHandlerCanBeConfiguredToUseStatusHelper(int status) throws Exception { + + //given: + HttpStatusRequestRejectedHandler sut = new HttpStatusRequestRejectedHandler(status); + HttpServletResponse response = mock(HttpServletResponse.class); + + //when: + sut.handle(mock(HttpServletRequest.class), response, mock(RequestRejectedException.class)); + + // then: + verify(response).sendError(status); + } +}