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 776fa183b7..8684a5a856 100644 --- a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java +++ b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java @@ -15,37 +15,37 @@ package org.springframework.security.web; -import java.io.IOException; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; +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.util.AntUrlPathMatcher; +import org.springframework.security.web.util.UrlMatcher; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.util.Assert; +import org.springframework.web.filter.DelegatingFilterProxy; +import org.springframework.web.filter.GenericFilterBean; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestWrapper; import javax.servlet.ServletResponse; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; -import org.springframework.security.web.util.AntUrlPathMatcher; -import org.springframework.security.web.util.UrlMatcher; -import org.springframework.util.Assert; -import org.springframework.web.filter.DelegatingFilterProxy; -import org.springframework.web.filter.GenericFilterBean; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; /** - * Delegates Filter requests to a list of Spring-managed beans. - * As of version 2.0, you shouldn't need to explicitly configure a FilterChainProxy bean in your application + * Delegates {@code Filter} requests to a list of Spring-managed filter beans. + * As of version 2.0, you shouldn't need to explicitly configure a {@code FilterChainProxy} bean in your application * context unless you need very fine control over the filter chain contents. Most cases should be adequately covered - * by the default <security:http /> namespace configuration options. + * by the default <security:http /> namespace configuration options. * *

The FilterChainProxy is loaded via a standard Spring {@link DelegatingFilterProxy} declaration in * web.xml. FilterChainProxy will then pass {@link #init(FilterConfig)}, {@link #destroy()} @@ -71,7 +71,7 @@ import org.springframework.web.filter.GenericFilterBean; </bean> * * - * The names "filter1", "filter2", "filter3" should be the bean names of Filter instances defined in the + * The names "filter1", "filter2", "filter3" should be the bean names of {@code Filter} instances defined in the * application context. The order of the names defines the order in which the filters will be applied. As shown above, * use of the value "none" for the "filters" can be used to exclude * Please consult the security namespace schema file for a full list of available configuration options. @@ -86,17 +86,32 @@ import org.springframework.web.filter.GenericFilterBean; * *

FilterChainProxy respects normal handling of Filters that elect not to call {@link * javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, - * javax.servlet.FilterChain)}, in that the remainder of the original or FilterChainProxy-declared filter + * javax.servlet.FilterChain)}, in that the remainder of the original or {@code FilterChainProxy}-declared filter * chain will not be called. * - *

Note the Filter lifecycle mismatch between the servlet container and IoC - * container. As described in the {@link DelegatingFilterProxy} JavaDocs, we recommend you allow the IoC - * container to manage the lifecycle instead of the servlet container. By default the DelegatingFilterProxy - * will never call this class' {@link #init(FilterConfig)} and {@link #destroy()} methods, which in turns means that - * the corresponding methods on the filter beans managed by this class will never be called. If you do need your filters to be - * initialized and destroyed, please set the targetFilterLifecycle initialization parameter against the - * DelegatingFilterProxy to specify that servlet container lifecycle management should be used. You don't - * need to worry about this in most cases. + *

Request Firewalling

+ * + * 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 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. + *

+ * Note that this means that you must use the Spring Security filters in combination with a {@code FilterChainProxy} + * if you want this protection. Don't define them explicitly in your {@code web.xml} file. + *

+ * {@code FilterChainProxy} will use the firewall instance to obtain both request and response objects which will be + * fed down the filter chain, so it is also possible to use this functionality to control the functionality of the + * response. When the request has passed through the security filter chain, the {@code reset} method will be called. + * With the default implementation this means that the original values of {@code servletPath} and {@code pathInfo} will + * be returned thereafter, instead of the modified ones used for security pattern matching. + * + *

Filter Lifecycle

+ *

+ * Note the {@code Filter} lifecycle mismatch between the servlet container and IoC + * container. As described in the {@link DelegatingFilterProxy} Javadocs, we recommend you allow the IoC + * container to manage the lifecycle instead of the servlet container. {@code FilterChainProxy} does not invoke the + * standard filter lifecycle methods on any filter beans that you add to the application context. * * @author Carlos Sanchez * @author Ben Alex @@ -118,6 +133,7 @@ public class FilterChainProxy extends GenericFilterBean { private Map> filterChainMap; private UrlMatcher matcher = new AntUrlPathMatcher(); private boolean stripQueryStringFromUrls = true; + private HttpFirewall firewall = new DefaultHttpFirewall(); private FilterChainValidator filterChainValidator = new NullFilterChainValidator(); //~ Methods ======================================================================================================== @@ -131,22 +147,24 @@ public class FilterChainProxy extends GenericFilterBean { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - FilterInvocation fi = new FilterInvocation(request, response, chain); - List filters = getFilters(fi.getRequestUrl()); + FirewalledRequest fwRequest = firewall.getFirewalledRequest((HttpServletRequest) request); + HttpServletResponse fwResponse = firewall.getFirewalledResponse((HttpServletResponse) response); + String url = UrlUtils.buildRequestUrl(fwRequest); + + List filters = getFilters(url); if (filters == null || filters.size() == 0) { if (logger.isDebugEnabled()) { - logger.debug(fi.getRequestUrl() + - filters == null ? " has no matching filters" : " has an empty filter list"); + logger.debug(url + (filters == null ? " has no matching filters" : " has an empty filter list")); } - chain.doFilter(request, response); + chain.doFilter(fwRequest, fwResponse); return; } - VirtualFilterChain virtualFilterChain = new VirtualFilterChain(fi, filters); - virtualFilterChain.doFilter(fi.getRequest(), fi.getResponse()); + VirtualFilterChain vfc = new VirtualFilterChain(url, chain, filters); + vfc.doFilter(fwRequest, fwResponse); } /** @@ -213,14 +231,14 @@ public class FilterChainProxy extends GenericFilterBean { /** * Sets the mapping of URL patterns to filter chains. * - * The map keys should be the paths and the values should be arrays of Filter objects. + * The map keys should be the paths and the values should be arrays of {@code Filter} objects. * It's VERY important that the type of map used preserves ordering - the order in which the iterator * returns the entries must be the same as the order they were added to the map, otherwise you have no way * of guaranteeing that the most specific patterns are returned before the more general ones. So make sure - * the Map used is an instance of LinkedHashMap or an equivalent, rather than a plain HashMap, for + * the Map used is an instance of {@code LinkedHashMap} or an equivalent, rather than a plain {@code HashMap}, for * example. * - * @param filterChainMap the map of path Strings to List<Filter>s. + * @param filterChainMap the map of path Strings to {@code List<Filter>}s. */ @SuppressWarnings("unchecked") public void setFilterChainMap(Map filterChainMap) { @@ -270,7 +288,7 @@ public class FilterChainProxy extends GenericFilterBean { /** * Returns a copy of the underlying filter chain map. Modifications to the map contents - * will not affect the FilterChainProxy state - to change the map call setFilterChainMap. + * will not affect the FilterChainProxy state - to change the map call {@code setFilterChainMap}. * * @return the map of path pattern Strings to filter chain lists (with ordering guaranteed). */ @@ -317,42 +335,53 @@ public class FilterChainProxy extends GenericFilterBean { //~ Inner Classes ================================================================================================== /** - * A FilterChain that records whether or not {@link - * FilterChain#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} is called. - *

- * This FilterChain is used by FilterChainProxy to determine if the next - * Filter should be called or not.

+ * Internal {@code FilterChain} implementation that is used to pass a request through the additional + * internal list of filters which match the request. */ private static class VirtualFilterChain implements FilterChain { - private FilterInvocation fi; - private List additionalFilters; + private final FilterChain originalChain; + private final List additionalFilters; + private final String url; private int currentPosition = 0; - private VirtualFilterChain(FilterInvocation filterInvocation, List additionalFilters) { - this.fi = filterInvocation; + private VirtualFilterChain(String url, FilterChain chain, List additionalFilters) { + this.originalChain = chain; + this.url = url; this.additionalFilters = additionalFilters; } - public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + public void doFilter(final ServletRequest request, final ServletResponse response) throws IOException, ServletException { if (currentPosition == additionalFilters.size()) { if (logger.isDebugEnabled()) { - logger.debug(fi.getRequestUrl() - + " reached end of additional filter chain; proceeding with original chain"); + logger.debug(url + " reached end of additional filter chain; proceeding with original chain"); } - fi.getChain().doFilter(request, response); + // Deactivate path stripping as we exit the security filter chain + resetWrapper(request); + + originalChain.doFilter(request, response); } else { currentPosition++; Filter nextFilter = additionalFilters.get(currentPosition - 1); if (logger.isDebugEnabled()) { - logger.debug(fi.getRequestUrl() + " at position " + currentPosition + " of " + logger.debug(url + " at position " + currentPosition + " of " + additionalFilters.size() + " in additional filter chain; firing Filter: '" - + nextFilter + "'"); + + nextFilter.getClass().getSimpleName() + "'"); } - nextFilter.doFilter(request, response, this); + nextFilter.doFilter(request, response, this); + } + } + + private void resetWrapper(ServletRequest request) { + while (request instanceof ServletRequestWrapper) { + if (request instanceof FirewalledRequest) { + ((FirewalledRequest)request).reset(); + break; + } + request = ((ServletRequestWrapper)request).getRequest(); } } } 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 new file mode 100644 index 0000000000..a40fa435dc --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/DefaultHttpFirewall.java @@ -0,0 +1,67 @@ +package org.springframework.security.web.firewall; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Default implementation which wraps requests in order to provide consistent values of the {@code servletPath} and + * {@code pathInfo}, which do not contain path parameters (as defined in + * RFC 2396). Different servlet containers + * interpret the servlet spec differently as to how path parameters are treated and it is possible they might be added + * in order to bypass particular security constraints. When using this implementation, they will be removed for all + * requests as the request passes through the security filter chain. Note that this means that any segments in the + * decoded path which contain a semi-colon, will have the part following the semi-colon removed for + * request matching. Your application should not contain any valid paths which contain semi-colons. + *

+ * If any un-normalized paths are found (containing directory-traversal character sequences), the request will be + * rejected immediately. Most containers normalize the paths before performing the servlet-mapping, but again this is + * not guaranteed by the servlet spec. + * + * @author Luke Taylor + */ +public class DefaultHttpFirewall implements HttpFirewall { + + public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException { + FirewalledRequest fwr = new RequestWrapper(request); + + if (!isNormalized(fwr.getServletPath()) || !isNormalized(fwr.getPathInfo())) { + throw new RequestRejectedException("Un-normalized paths are not supported: " + fwr.getServletPath() + + (fwr.getPathInfo() != null ? fwr.getPathInfo() : "")); + } + + return fwr; + } + + public HttpServletResponse getFirewalledResponse(HttpServletResponse response) { + return response; + } + + /** + * 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 boolean isNormalized(String path) { + if (path == null) { + return true; + } + + 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/main/java/org/springframework/security/web/firewall/FirewalledRequest.java b/web/src/main/java/org/springframework/security/web/firewall/FirewalledRequest.java new file mode 100644 index 0000000000..48cf40acc2 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/FirewalledRequest.java @@ -0,0 +1,34 @@ +package org.springframework.security.web.firewall; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +/** + * Request wrapper which is returned by the {@code HttpFirewall} interface. + *

+ * The only difference is the {@code reset} method which allows some + * or all of the state to be reset by the {@code FilterChainProxy} when the + * request leaves the security filter chain. + * + * @author Luke Taylor + */ +public abstract class FirewalledRequest extends HttpServletRequestWrapper { + /** + * Constructs a request object wrapping the given request. + * + * @throws IllegalArgumentException if the request is null + */ + public FirewalledRequest(HttpServletRequest request) { + super(request); + } + + /** + * This method will be called once the request has passed through the + * security filter chain, when it is about to proceed to the application + * proper. + *

+ * An implementation can thus choose to modify the state of the request + * for the security infrastructure, while still maintaining the + */ + public abstract void reset(); +} diff --git a/web/src/main/java/org/springframework/security/web/firewall/HttpFirewall.java b/web/src/main/java/org/springframework/security/web/firewall/HttpFirewall.java new file mode 100644 index 0000000000..de7b9af17e --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/HttpFirewall.java @@ -0,0 +1,32 @@ +package org.springframework.security.web.firewall; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Interface which can be used to reject potentially dangerous requests and/or wrap them to + * control their behaviour. + *

+ * The implementation is injected into the {@code FilterChainProxy} and will be invoked before + * sending any request through the filter chain. It can also provide a response wrapper if the response + * behaviour should also be restricted. + * + * @author Luke Taylor + */ +public interface HttpFirewall { + + /** + * Provides the request object which will be passed through the filter chain. + * + * @throws RequestRejectedException if the request should be rejected immediately + */ + FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException; + + /** + * Provides the response which will be passed through the filter chain. + * + * @param response the original response + * @return either the original response or a replacement/wrapper. + */ + HttpServletResponse getFirewalledResponse(HttpServletResponse response); +} diff --git a/web/src/main/java/org/springframework/security/web/firewall/RequestRejectedException.java b/web/src/main/java/org/springframework/security/web/firewall/RequestRejectedException.java new file mode 100644 index 0000000000..fe334219a0 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/RequestRejectedException.java @@ -0,0 +1,10 @@ +package org.springframework.security.web.firewall; + +/** + * @author Luke Taylor + */ +public class RequestRejectedException extends RuntimeException { + public RequestRejectedException(String message) { + super(message); + } +} diff --git a/web/src/main/java/org/springframework/security/web/firewall/RequestWrapper.java b/web/src/main/java/org/springframework/security/web/firewall/RequestWrapper.java new file mode 100644 index 0000000000..1c6e25de8e --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/firewall/RequestWrapper.java @@ -0,0 +1,100 @@ +package org.springframework.security.web.firewall; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * Request wrapper which ensures values of {@code servletPath} and {@code pathInfo} are returned which are suitable for + * pattern matching against. It strips out path parameters and extra consecutive '/' characters. + * + *

Path Parameters

+ * Parameters (as defined in RFC 2396) are stripped from the path + * segments of the {@code servletPath} and {@code pathInfo} values of the request. + *

+ * The parameter sequence is demarcated by a semi-colon, so each segment is checked for the occurrence of a ";" + * character and truncated at that point if it is present. + *

+ * The behaviour differs between servlet containers in how they interpret the servlet spec, which + * does not clearly state what the behaviour should be. For consistency, we make sure they are always removed, to + * avoid the risk of URL matching rules being bypassed by the malicious addition of parameters to the path component. + * + * @author Luke Taylor + */ +final class RequestWrapper extends FirewalledRequest { + private final String strippedServletPath; + private final String strippedPathInfo; + private boolean stripPaths = true; + + public RequestWrapper(HttpServletRequest request) { + super(request); + strippedServletPath = strip(request.getServletPath()); + String pathInfo = strip(request.getPathInfo()); + if (pathInfo != null && pathInfo.length() == 0) { + pathInfo = null; + } + strippedPathInfo = pathInfo; + } + + /** + * Removes path parameters from each path segment in the supplied path and truncates sequences of multiple '/' + * characters to a single '/'. + * + * @param path either the {@code servletPath} and {@code pathInfo} from the original request + * + * @return the supplied value, with path parameters removed and sequences of multiple '/' characters truncated, + * or null if the supplied path was null. + */ + private String strip(String path) { + if (path == null) { + return null; + } + + int scIndex = path.indexOf(';'); + + if (scIndex < 0) { + int doubleSlashIndex = path.indexOf("//"); + if (doubleSlashIndex < 0) { + // Most likely case, no parameters in any segment and no '//', so no stripping required + return path; + } + } + + StringTokenizer st = new StringTokenizer(path, "/"); + StringBuilder stripped = new StringBuilder(path.length()); + + if (path.charAt(0) == '/') { + stripped.append('/'); + } + + while(st.hasMoreTokens()) { + String segment = st.nextToken(); + scIndex = segment.indexOf(';'); + + if (scIndex >= 0) { + segment = segment.substring(0, scIndex); + } + stripped.append(segment).append('/'); + } + + // Remove the trailing slash if the original path didn't have one + if (path.charAt(path.length() - 1) != '/') { + stripped.deleteCharAt(stripped.length() - 1); + } + + return stripped.toString(); + } + + @Override + public String getPathInfo() { + return stripPaths ? strippedPathInfo : super.getPathInfo(); + } + + @Override + public String getServletPath() { + return stripPaths ? strippedServletPath : super.getServletPath(); + } + + public void reset() { + this.stripPaths = false; + } +} diff --git a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java new file mode 100644 index 0000000000..a381b44f09 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java @@ -0,0 +1,103 @@ +package org.springframework.security.web; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.web.firewall.FirewalledRequest; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.*; + +/** + * @author Luke Taylor + */ +@SuppressWarnings({"unchecked"}) +public class FilterChainProxyTests { + private FilterChainProxy fcp; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private FilterChain chain; + private Filter filter; + + @Before + public void setup() throws Exception { + fcp = new FilterChainProxy(); + filter = mock(Filter.class); + doAnswer(new Answer() { + public Object answer(InvocationOnMock inv) throws Throwable { + Object[] args = inv.getArguments(); + FilterChain fc = (FilterChain) args[2]; + fc.doFilter((HttpServletRequest) args[0], (HttpServletResponse) args[1]); + return null; + } + }).when(filter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), any(FilterChain.class)); + LinkedHashMap map = new LinkedHashMap(); + map.put("/match", Arrays.asList(filter)); + fcp.setFilterChainMap(map); + request = new MockHttpServletRequest(); + request.setServletPath("/match"); + response = new MockHttpServletResponse(); + chain = mock(FilterChain.class); + } + + @Test + public void toStringCallSucceeds() throws Exception { + fcp.afterPropertiesSet(); + fcp.toString(); + } + + @Test + public void securityFilterChainIsNotInvokedIfMatchFails() throws Exception { + request.setServletPath("/nomatch"); + fcp.doFilter(request, response, chain); + assertEquals(1, fcp.getFilterChainMap().size()); + + verifyZeroInteractions(filter); + // The actual filter chain should be invoked though + verify(chain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void originalChainIsInvokedAfterSecurityChainIfMatchSucceeds() throws Exception { + fcp.doFilter(request, response, chain); + + verify(filter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), any(FilterChain.class)); + verify(chain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void originalFilterChainIsInvokedIfMatchingSecurityChainIsEmpty() throws Exception { + LinkedHashMap map = new LinkedHashMap(); + map.put("/match", Collections.emptyList()); + fcp.setFilterChainMap(map); + + fcp.doFilter(request, response, chain); + + verify(chain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void requestIsWrappedForFilteringWhenMatchIsFound() throws Exception { + fcp.doFilter(request, response, chain); + verify(filter).doFilter(any(FirewalledRequest.class), any(HttpServletResponse.class), any(FilterChain.class)); + verify(chain).doFilter(any(FirewalledRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void requestIsWrappedForFilteringWhenMatchIsNotFound() throws Exception { + request.setServletPath("/nomatch"); + fcp.doFilter(request, response, chain); + verifyZeroInteractions(filter); + verify(chain).doFilter(any(FirewalledRequest.class), any(HttpServletResponse.class)); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/firewall/DefaultHttpFirewallTests.java b/web/src/test/java/org/springframework/security/web/firewall/DefaultHttpFirewallTests.java new file mode 100644 index 0000000000..8b3df587e9 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/firewall/DefaultHttpFirewallTests.java @@ -0,0 +1,44 @@ +package org.springframework.security.web.firewall; + +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * @author Luke Taylor + */ +public class DefaultHttpFirewallTests { + public String[] unnormalizedPaths = { + "/..", + "/./path/", + "/path/path/.", + "/path/path//.", + "./path/../path//.", + "./path", + ".//path", + "." + }; + + @Test + public void unnormalizedPathsAreRejected() throws Exception { + DefaultHttpFirewall fw = new DefaultHttpFirewall(); + + MockHttpServletRequest request; + for (String path : unnormalizedPaths) { + request = new MockHttpServletRequest(); + request.setServletPath(path); + try { + fw.getFirewalledRequest(request); + fail(path + " is un-normalized"); + } catch (RequestRejectedException expected) { + } + request.setPathInfo(path); + try { + fw.getFirewalledRequest(request); + fail(path + " is un-normalized"); + } catch (RequestRejectedException expected) { + } + } + } +} diff --git a/web/src/test/java/org/springframework/security/web/firewall/RequestWrapperTests.java b/web/src/test/java/org/springframework/security/web/firewall/RequestWrapperTests.java new file mode 100644 index 0000000000..b462251440 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/firewall/RequestWrapperTests.java @@ -0,0 +1,62 @@ +package org.springframework.security.web.firewall; + +import static org.junit.Assert.assertEquals; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.util.*; + +/** + * @author Luke Taylor + */ +public class RequestWrapperTests { + private static Map testPaths = new LinkedHashMap(); + + @BeforeClass + // Some of these may be unrealistic values, but we can't be sure because of the + // inconsistency in the spec. + public static void createTestMap() { + testPaths.put("/path1;x=y;z=w/path2;x=y/path3;x=y", "/path1/path2/path3"); + testPaths.put("/path1;x=y/path2;x=y/", "/path1/path2/"); + testPaths.put("/path1//path2/", "/path1/path2/"); + testPaths.put("//path1/path2//", "/path1/path2/"); + testPaths.put(";x=y;z=w", ""); + } + + @Test + public void pathParametersAreRemovedFromServletPath() { + MockHttpServletRequest request = new MockHttpServletRequest(); + + for (Map.Entry entry : testPaths.entrySet()) { + String path = entry.getKey(); + String expectedResult = entry.getValue(); + request.setServletPath(path); + RequestWrapper wrapper = new RequestWrapper(request); + assertEquals(expectedResult, wrapper.getServletPath()); + wrapper.reset(); + assertEquals(path, wrapper.getServletPath()); + } + } + + @Test + public void pathParametersAreRemovedFromPathInfo() { + MockHttpServletRequest request = new MockHttpServletRequest(); + + for (Map.Entry entry : testPaths.entrySet()) { + String path = entry.getKey(); + String expectedResult = entry.getValue(); + // Should be null when stripped value is empty + if (expectedResult.length() == 0) { + expectedResult = null; + } + request.setPathInfo(path); + RequestWrapper wrapper = new RequestWrapper(request); + assertEquals(expectedResult, wrapper.getPathInfo()); + wrapper.reset(); + assertEquals(path, wrapper.getPathInfo()); + } + } + +}