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 e5f15b5f56..7dae5a20c3 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 @@ -47,7 +47,7 @@ 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.DefaultHttpFirewall; +import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.firewall.HttpFirewall; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -159,7 +159,7 @@ public final class WebSecurity extends /** * Allows customizing the {@link HttpFirewall}. The default is - * {@link DefaultHttpFirewall}. + * {@link StrictHttpFirewall}. * * @param httpFirewall the custom {@link HttpFirewall} * @return the {@link WebSecurity} for further customizations diff --git a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java index 5a64a9c927..ba898c74ae 100644 --- a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java @@ -38,6 +38,7 @@ import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.security.web.firewall.DefaultHttpFirewall; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; @@ -80,6 +81,7 @@ public class FilterChainProxyConfigTests { public void normalOperationWithNewConfig() throws Exception { FilterChainProxy filterChainProxy = appCtx.getBean("newFilterChainProxy", FilterChainProxy.class); + filterChainProxy.setFirewall(new DefaultHttpFirewall()); checkPathAndFilterOrder(filterChainProxy); doNormalOperation(filterChainProxy); } @@ -88,6 +90,7 @@ public class FilterChainProxyConfigTests { public void normalOperationWithNewConfigRegex() throws Exception { FilterChainProxy filterChainProxy = appCtx.getBean("newFilterChainProxyRegex", FilterChainProxy.class); + filterChainProxy.setFirewall(new DefaultHttpFirewall()); checkPathAndFilterOrder(filterChainProxy); doNormalOperation(filterChainProxy); } @@ -96,6 +99,7 @@ public class FilterChainProxyConfigTests { public void normalOperationWithNewConfigNonNamespace() throws Exception { FilterChainProxy filterChainProxy = appCtx.getBean( "newFilterChainProxyNonNamespace", FilterChainProxy.class); + filterChainProxy.setFirewall(new DefaultHttpFirewall()); checkPathAndFilterOrder(filterChainProxy); doNormalOperation(filterChainProxy); } diff --git a/itest/context/src/integration-test/java/org/springframework/security/integration/HttpPathParameterStrippingTests.java b/itest/context/src/integration-test/java/org/springframework/security/integration/HttpPathParameterStrippingTests.java index 8567104cab..1aceab6e11 100644 --- a/itest/context/src/integration-test/java/org/springframework/security/integration/HttpPathParameterStrippingTests.java +++ b/itest/context/src/integration-test/java/org/springframework/security/integration/HttpPathParameterStrippingTests.java @@ -28,6 +28,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.firewall.RequestRejectedException; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -40,7 +41,7 @@ public class HttpPathParameterStrippingTests { @Autowired private FilterChainProxy fcp; - @Test + @Test(expected = RequestRejectedException.class) public void securedFilterChainCannotBeBypassedByAddingPathParameters() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); @@ -48,24 +49,25 @@ public class HttpPathParameterStrippingTests { request.setSession(createAuthenticatedSession("ROLE_USER")); MockHttpServletResponse response = new MockHttpServletResponse(); fcp.doFilter(request, response, new MockFilterChain()); - assertThat(response.getStatus()).isEqualTo(403); } - @Test + @Test(expected = RequestRejectedException.class) public void adminFilePatternCannotBeBypassedByAddingPathParameters() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setServletPath("/secured/admin.html;x=user.html"); request.setSession(createAuthenticatedSession("ROLE_USER")); MockHttpServletResponse response = new MockHttpServletResponse(); fcp.doFilter(request, response, new MockFilterChain()); - assertThat(response.getStatus()).isEqualTo(403); + } - // Try with pathInfo - request = new MockHttpServletRequest(); + + @Test(expected = RequestRejectedException.class) + public void adminFilePatternCannotBeBypassedByAddingPathParametersWithPathInfo() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); request.setServletPath("/secured"); request.setPathInfo("/admin.html;x=user.html"); request.setSession(createAuthenticatedSession("ROLE_USER")); - response = new MockHttpServletResponse(); + MockHttpServletResponse response = new MockHttpServletResponse(); fcp.doFilter(request, response, new MockFilterChain()); assertThat(response.getStatus()).isEqualTo(403); } 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 e1e9ce9037..d7823ea6dd 100644 --- a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java +++ b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java @@ -19,9 +19,9 @@ 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.DefaultHttpFirewall; import org.springframework.security.web.firewall.FirewalledRequest; import org.springframework.security.web.firewall.HttpFirewall; +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.web.filter.DelegatingFilterProxy; @@ -96,7 +96,7 @@ import java.util.*; * * An {@link HttpFirewall} instance is used to validate incoming requests and create a * wrapped request which provides consistent path values for matching against. See - * {@link DefaultHttpFirewall}, for more information on the type of attacks which the + * {@link StrictHttpFirewall}, for more information on the type of attacks which the * default implementation protects against. A custom implementation can be injected to * provide stricter control over the request contents or if an application needs to * support certain types of request which are rejected by default. @@ -147,7 +147,7 @@ public class FilterChainProxy extends GenericFilterBean { private FilterChainValidator filterChainValidator = new NullFilterChainValidator(); - private HttpFirewall firewall = new DefaultHttpFirewall(); + private HttpFirewall firewall = new StrictHttpFirewall(); // ~ Methods // ======================================================================================================== diff --git a/web/src/main/java/org/springframework/security/web/firewall/DefaultHttpFirewall.java b/web/src/main/java/org/springframework/security/web/firewall/DefaultHttpFirewall.java index a63035a038..304fcff212 100644 --- a/web/src/main/java/org/springframework/security/web/firewall/DefaultHttpFirewall.java +++ b/web/src/main/java/org/springframework/security/web/firewall/DefaultHttpFirewall.java @@ -37,8 +37,10 @@ import javax.servlet.http.HttpServletResponse; * containers normalize the paths before performing the servlet-mapping, but * again this is not guaranteed by the servlet spec. * + * @deprecated Use {@link StrictHttpFirewall} instead * @author Luke Taylor */ +@Deprecated public class DefaultHttpFirewall implements HttpFirewall { private boolean allowUrlEncodedSlash; diff --git a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java new file mode 100644 index 0000000000..b97f56ee47 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java @@ -0,0 +1,239 @@ +/* + * Copyright 2012-2017 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 + * + * 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 org.springframework.security.web.firewall; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A strict implementation of {@link HttpFirewall} that rejects any suspicious requests + * with a {@link RequestRejectedException}. + * + * @author Rob Winch + * @since 5.0.1 + */ +public class StrictHttpFirewall implements HttpFirewall { + private static final String ENCODED_PERCENT = "%25"; + + private static final String PERCENT = "%"; + + private static final List FORBIDDEN_ENCODED_PERIOD = Collections.unmodifiableList(Arrays.asList("%2e", "%2E")); + + private static final List FORBIDDEN_SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B")); + + private static final List FORBIDDEN_FORWARDSLASH = Collections.unmodifiableList(Arrays.asList("%2f", "%2F")); + + private static final List FORBIDDEN_BACKSLASH = Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C")); + + private Set encodedUrlBlacklist = new HashSet(); + + private Set decodedUrlBlacklist = new HashSet(); + + public StrictHttpFirewall() { + urlBlacklistsAddAll(FORBIDDEN_SEMICOLON); + urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH); + urlBlacklistsAddAll(FORBIDDEN_BACKSLASH); + + this.encodedUrlBlacklist.add(ENCODED_PERCENT); + this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD); + this.decodedUrlBlacklist.add(PERCENT); + } + + /** + * + * @param allowSemicolon + */ + public void setAllowSemicolon(boolean allowSemicolon) { + if (allowSemicolon) { + urlBlacklistsRemoveAll(FORBIDDEN_SEMICOLON); + } else { + urlBlacklistsAddAll(FORBIDDEN_SEMICOLON); + } + } + + public void setAllowUrlEncodedSlash(boolean allowUrlEncodedSlash) { + if (allowUrlEncodedSlash) { + urlBlacklistsRemoveAll(FORBIDDEN_FORWARDSLASH); + } else { + urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH); + } + } + + public void setAllowUrlEncodedPeriod(boolean allowUrlEncodedPeriod) { + if (allowUrlEncodedPeriod) { + this.encodedUrlBlacklist.removeAll(FORBIDDEN_ENCODED_PERIOD); + } else { + this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD); + } + } + + public void setAllowBackSlash(boolean allowBackSlash) { + if (allowBackSlash) { + urlBlacklistsRemoveAll(FORBIDDEN_BACKSLASH); + } else { + urlBlacklistsAddAll(FORBIDDEN_BACKSLASH); + } + } + + public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) { + if (allowUrlEncodedPercent) { + this.encodedUrlBlacklist.remove(ENCODED_PERCENT); + this.decodedUrlBlacklist.remove(PERCENT); + } else { + this.encodedUrlBlacklist.add(ENCODED_PERCENT); + this.decodedUrlBlacklist.add(PERCENT); + } + } + + private void urlBlacklistsAddAll(Collection values) { + this.encodedUrlBlacklist.addAll(values); + this.decodedUrlBlacklist.addAll(values); + } + + private void urlBlacklistsRemoveAll(Collection values) { + this.encodedUrlBlacklist.removeAll(values); + this.decodedUrlBlacklist.removeAll(values); + } + + @Override + public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException { + rejectedBlacklistedUrls(request); + + if (!isNormalized(request)) { + throw new RequestRejectedException("The request was rejected because the URL was not normalized."); + } + + String requestUri = request.getRequestURI(); + if (!containsOnlyPrintableAsciiCharacters(requestUri)) { + throw new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters."); + } + return new FirewalledRequest(request) { + @Override + public void reset() { + } + }; + } + + private void rejectedBlacklistedUrls(HttpServletRequest request) { + for (String forbidden : this.encodedUrlBlacklist) { + if (encodedUrlContains(request, forbidden)) { + throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\""); + } + } + for (String forbidden : this.decodedUrlBlacklist) { + if (decodedUrlContains(request, forbidden)) { + throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\""); + } + } + } + + @Override + public HttpServletResponse getFirewalledResponse(HttpServletResponse response) { + return new FirewalledResponse(response); + } + + private static boolean isNormalized(HttpServletRequest request) { + if (!isNormalized(request.getRequestURI())) { + return false; + } + if (!isNormalized(request.getContextPath())) { + return false; + } + if (!isNormalized(request.getServletPath())) { + return false; + } + if (!isNormalized(request.getPathInfo())) { + return false; + } + return true; + } + + private static boolean encodedUrlContains(HttpServletRequest request, String value) { + if (valueContains(request.getContextPath(), value)) { + return true; + } + return valueContains(request.getRequestURI(), value); + } + + private static boolean decodedUrlContains(HttpServletRequest request, String value) { + if (valueContains(request.getServletPath(), value)) { + return true; + } + if (valueContains(request.getPathInfo(), value)) { + return true; + } + return false; + } + + private static boolean containsOnlyPrintableAsciiCharacters(String uri) { + int length = uri.length(); + for (int i = 0; i < length; i++) { + char c = uri.charAt(i); + if (c < '\u0021' || '\u007e' < c) { + return false; + } + } + + return true; + } + + private static boolean valueContains(String value, String contains) { + return value != null && value.contains(contains); + } + + /** + * Checks whether a path is normalized (doesn't contain path traversal + * sequences like "./", "/../" or "/.") + * + * @param path + * the path to test + * @return true if the path doesn't contain any path-traversal character + * sequences. + */ + private static boolean isNormalized(String path) { + if (path == null) { + return true; + } + + if (path.indexOf("//") > 0) { + return false; + } + + for (int j = path.length(); j > 0;) { + int i = path.lastIndexOf('/', j - 1); + int gap = j - i; + + if (gap == 2 && path.charAt(i + 1) == '.') { + // ".", "/./" or "/." + return false; + } else if (gap == 3 && path.charAt(i + 1) == '.' && path.charAt(i + 2) == '.') { + return false; + } + + j = i; + } + + return true; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java new file mode 100644 index 0000000000..c9bbc654a1 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java @@ -0,0 +1,351 @@ +/* + * Copyright 2012-2017 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 + * + * 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 org.springframework.security.web.firewall; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.fail; + +/** + * @author Rob Winch + */ +public class StrictHttpFirewallTests { + public String[] unnormalizedPaths = { "/..", "/./path/", "/path/path/.", "/path/path//.", "./path/../path//.", + "./path", ".//path", ".", "/path//" }; + + private StrictHttpFirewall firewall = new StrictHttpFirewall(); + + private MockHttpServletRequest request = new MockHttpServletRequest(); + + @Test + public void getFirewalledRequestWhenRequestURINotNormalizedThenThrowsRequestRejectedException() throws Exception { + for (String path : this.unnormalizedPaths) { + this.request = new MockHttpServletRequest(); + this.request.setRequestURI(path); + try { + this.firewall.getFirewalledRequest(this.request); + fail(path + " is un-normalized"); + } catch (RequestRejectedException expected) { + } + } + } + + @Test + public void getFirewalledRequestWhenContextPathNotNormalizedThenThrowsRequestRejectedException() throws Exception { + for (String path : this.unnormalizedPaths) { + this.request = new MockHttpServletRequest(); + this.request.setContextPath(path); + try { + this.firewall.getFirewalledRequest(this.request); + fail(path + " is un-normalized"); + } catch (RequestRejectedException expected) { + } + } + } + + @Test + public void getFirewalledRequestWhenServletPathNotNormalizedThenThrowsRequestRejectedException() throws Exception { + for (String path : this.unnormalizedPaths) { + this.request = new MockHttpServletRequest(); + this.request.setServletPath(path); + try { + this.firewall.getFirewalledRequest(this.request); + fail(path + " is un-normalized"); + } catch (RequestRejectedException expected) { + } + } + } + + @Test + public void getFirewalledRequestWhenPathInfoNotNormalizedThenThrowsRequestRejectedException() throws Exception { + for (String path : this.unnormalizedPaths) { + this.request = new MockHttpServletRequest(); + this.request.setPathInfo(path); + try { + this.firewall.getFirewalledRequest(this.request); + fail(path + " is un-normalized"); + } catch (RequestRejectedException expected) { + } + } + } + + // --- ; --- + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenSemicolonInContextPathThenThrowsRequestRejectedException() { + this.request.setContextPath(";/context"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenSemicolonInServletPathThenThrowsRequestRejectedException() { + this.request.setServletPath("/spring;/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenSemicolonInPathInfoThenThrowsRequestRejectedException() { + this.request.setPathInfo("/path;/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenSemicolonInRequestUriThenThrowsRequestRejectedException() { + this.request.setRequestURI("/path;/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenEncodedSemicolonInContextPathThenThrowsRequestRejectedException() { + this.request.setContextPath("%3B/context"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenEncodedSemicolonInServletPathThenThrowsRequestRejectedException() { + this.request.setServletPath("/spring%3B/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenEncodedSemicolonInPathInfoThenThrowsRequestRejectedException() { + this.request.setPathInfo("/path%3B/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenEncodedSemicolonInRequestUriThenThrowsRequestRejectedException() { + this.request.setRequestURI("/path%3B/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenLowercaseEncodedSemicolonInContextPathThenThrowsRequestRejectedException() { + this.request.setContextPath("%3b/context"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenLowercaseEncodedSemicolonInServletPathThenThrowsRequestRejectedException() { + this.request.setServletPath("/spring%3b/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenLowercaseEncodedSemicolonInPathInfoThenThrowsRequestRejectedException() { + this.request.setPathInfo("/path%3b/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenLowercaseEncodedSemicolonInRequestUriThenThrowsRequestRejectedException() { + this.request.setRequestURI("/path%3b/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenSemicolonInContextPathAndAllowSemicolonThenNoException() { + this.firewall.setAllowSemicolon(true); + this.request.setContextPath(";/context"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenSemicolonInServletPathAndAllowSemicolonThenNoException() { + this.firewall.setAllowSemicolon(true); + this.request.setServletPath("/spring;/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenSemicolonInPathInfoAndAllowSemicolonThenNoException() { + this.firewall.setAllowSemicolon(true); + this.request.setPathInfo("/path;/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenSemicolonInRequestUriAndAllowSemicolonThenNoException() { + this.firewall.setAllowSemicolon(true); + this.request.setRequestURI("/path;/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenEncodedSemicolonInContextPathAndAllowSemicolonThenNoException() { + this.firewall.setAllowUrlEncodedPercent(true); + this.firewall.setAllowSemicolon(true); + this.request.setContextPath("%3B/context"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenEncodedSemicolonInServletPathAndAllowSemicolonThenNoException() { + this.firewall.setAllowUrlEncodedPercent(true); + this.firewall.setAllowSemicolon(true); + this.request.setServletPath("/spring%3B/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenEncodedSemicolonInPathInfoAndAllowSemicolonThenNoException() { + this.firewall.setAllowUrlEncodedPercent(true); + this.firewall.setAllowSemicolon(true); + this.request.setPathInfo("/path%3B/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenEncodedSemicolonInRequestUriAndAllowSemicolonThenNoException() { + this.firewall.setAllowSemicolon(true); + this.request.setRequestURI("/path%3B/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenLowercaseEncodedSemicolonInContextPathAndAllowSemicolonThenNoException() { + this.firewall.setAllowUrlEncodedPercent(true); + this.firewall.setAllowSemicolon(true); + this.request.setContextPath("%3b/context"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenLowercaseEncodedSemicolonInServletPathAndAllowSemicolonThenNoException() { + this.firewall.setAllowUrlEncodedPercent(true); + this.firewall.setAllowSemicolon(true); + this.request.setServletPath("/spring%3b/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenLowercaseEncodedSemicolonInPathInfoAndAllowSemicolonThenNoException() { + this.firewall.setAllowUrlEncodedPercent(true); + this.firewall.setAllowSemicolon(true); + this.request.setPathInfo("/path%3b/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenLowercaseEncodedSemicolonInRequestUriAndAllowSemicolonThenNoException() { + this.firewall.setAllowSemicolon(true); + this.request.setRequestURI("/path%3b/"); + + this.firewall.getFirewalledRequest(this.request); + } + + // --- encoded . --- + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenEncodedPeriodInThenThrowsRequestRejectedException() { + this.request.setRequestURI("/%2E/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenLowercaseEncodedPeriodInThenThrowsRequestRejectedException() { + this.request.setRequestURI("/%2e/"); + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenAllowEncodedPeriodAndEncodedPeriodInThenNoException() { + this.firewall.setAllowUrlEncodedPeriod(true); + this.request.setRequestURI("/%2E/"); + + this.firewall.getFirewalledRequest(this.request); + } + + // --- from DefaultHttpFirewallTests --- + + /** + * On WebSphere 8.5 a URL like /context-root/a/b;%2f1/c can bypass a rule on + * /a/b/c because the pathInfo is /a/b;/1/c which ends up being /a/b/1/c + * while Spring MVC will strip the ; content from requestURI before the path + * is URL decoded. + */ + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenLowercaseEncodedPathThenException() { + this.request.setRequestURI("/context-root/a/b;%2f1/c"); + this.request.setContextPath("/context-root"); + this.request.setServletPath(""); + this.request.setPathInfo("/a/b;/1/c"); // URL decoded requestURI + this.firewall.getFirewalledRequest(this.request); + } + + @Test(expected = RequestRejectedException.class) + public void getFirewalledRequestWhenUppercaseEncodedPathThenException() { + this.request.setRequestURI("/context-root/a/b;%2F1/c"); + this.request.setContextPath("/context-root"); + this.request.setServletPath(""); + this.request.setPathInfo("/a/b;/1/c"); // URL decoded requestURI + + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenAllowUrlEncodedSlashAndLowercaseEncodedPathThenNoException() { + this.firewall.setAllowUrlEncodedSlash(true); + this.firewall.setAllowSemicolon(true); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/context-root/a/b;%2f1/c"); + request.setContextPath("/context-root"); + request.setServletPath(""); + request.setPathInfo("/a/b;/1/c"); // URL decoded requestURI + + this.firewall.getFirewalledRequest(request); + } + + @Test + public void getFirewalledRequestWhenAllowUrlEncodedSlashAndUppercaseEncodedPathThenNoException() { + this.firewall.setAllowUrlEncodedSlash(true); + this.firewall.setAllowSemicolon(true); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/context-root/a/b;%2F1/c"); + request.setContextPath("/context-root"); + request.setServletPath(""); + request.setPathInfo("/a/b;/1/c"); // URL decoded requestURI + + this.firewall.getFirewalledRequest(request); + } +}