Spring Security and Spring Framework may not recognize certain paths that should be protected
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
Details
Both Spring Security 3.2.x, 4.0.x, 4.1.0 and the Spring Framework 3.2.x, 4.0.x, 4.1.x, 4.2.x (as well as other unsupported versions) rely on URL pattern mappings for authorization and for mapping requests to controllers respectively. Differences in the strictness of the pattern matching mechanisms, for example with regards to space trimming in path segments, can lead Spring Security to not recognize certain paths as not protected that are in fact mapped to Spring MVC controllers that should be protected. The problem is compounded by the fact that the Spring Framework provides richer features with regards to pattern matching as well as by the fact that pattern matching in each Spring Security and the Spring Framework can easily be customized creating additional differences.
The fix
Introduce HandlerMapping introspection API
spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java+19 −9
@@ -27,21 +27,21 @@import org.springframework.beans.BeansException;import org.springframework.beans.factory.BeanFactoryUtils;import org.springframework.core.Ordered;-import org.springframework.web.HttpRequestHandler;-import org.springframework.web.cors.UrlBasedCorsConfigurationSource;-import org.springframework.web.cors.CorsProcessor;-import org.springframework.web.cors.CorsConfiguration;-import org.springframework.web.cors.CorsConfigurationSource;import org.springframework.util.AntPathMatcher;import org.springframework.util.Assert;import org.springframework.util.PathMatcher;+import org.springframework.web.HttpRequestHandler;import org.springframework.web.context.request.WebRequestInterceptor;import org.springframework.web.context.support.WebApplicationObjectSupport;+import org.springframework.web.cors.CorsConfiguration;+import org.springframework.web.cors.CorsConfigurationSource;+import org.springframework.web.cors.CorsProcessor;+import org.springframework.web.cors.CorsUtils;+import org.springframework.web.cors.DefaultCorsProcessor;+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import org.springframework.web.servlet.HandlerExecutionChain;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.HandlerMapping;-import org.springframework.web.cors.DefaultCorsProcessor;-import org.springframework.web.cors.CorsUtils;import org.springframework.web.util.UrlPathHelper;/**@@ -471,7 +471,7 @@ protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest}-private class PreFlightHandler implements HttpRequestHandler {+private class PreFlightHandler implements HttpRequestHandler, CorsConfigurationSource {private final CorsConfiguration config;@@ -485,10 +485,15 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse responcorsProcessor.processRequest(this.config, request, response);}++@Override+public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {+return this.config;+}}-private class CorsInterceptor extends HandlerInterceptorAdapter {+private class CorsInterceptor extends HandlerInterceptorAdapter implements CorsConfigurationSource {private final CorsConfiguration config;@@ -502,6 +507,11 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse responsreturn corsProcessor.processRequest(this.config, request, response);}++@Override+public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {+return this.config;+}}}
spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java+18 −1
@@ -30,6 +30,8 @@import org.springframework.util.CollectionUtils;import org.springframework.web.servlet.HandlerExecutionChain;import org.springframework.web.servlet.HandlerMapping;+import org.springframework.web.servlet.support.MatchableHandlerMapping;+import org.springframework.web.servlet.support.RequestMatchResult;/*** Abstract base class for URL-mapped {@link org.springframework.web.servlet.HandlerMapping}@@ -50,7 +52,8 @@* @author Arjen Poutsma* @since 16.04.2003*/-public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {+public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping+implements MatchableHandlerMapping {private Object rootHandler;@@ -279,6 +282,20 @@ protected void exposeUriTemplateVariables(Map<String, String> uriTemplateVariablrequest.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);}+@Override+public RequestMatchResult match(HttpServletRequest request, String pattern) {+String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);+if (getPathMatcher().match(pattern, lookupPath)) {+return new RequestMatchResult(pattern, lookupPath, getPathMatcher());+}+else if (useTrailingSlashMatch()) {+if (!pattern.endsWith("/") && getPathMatcher().match(pattern + "/", lookupPath)) {+return new RequestMatchResult(pattern + "/", lookupPath, getPathMatcher());+}+}+return null;+}+/*** Register the specified handler for the given URL paths.* @param urlPaths the URLs that the bean should be mapped to
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java+17 −1
@@ -20,6 +20,8 @@import java.lang.reflect.Method;import java.util.Arrays;import java.util.List;+import java.util.Set;+import javax.servlet.http.HttpServletRequest;import org.springframework.context.EmbeddedValueResolverAware;import org.springframework.core.annotation.AnnotatedElementUtils;@@ -38,6 +40,8 @@import org.springframework.web.servlet.mvc.condition.RequestCondition;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;+import org.springframework.web.servlet.support.MatchableHandlerMapping;+import org.springframework.web.servlet.support.RequestMatchResult;/*** Creates {@link RequestMappingInfo} instances from type and method-level@@ -50,7 +54,7 @@* @since 3.1*/public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping-implements EmbeddedValueResolverAware {+implements EmbeddedValueResolverAware, MatchableHandlerMapping {private boolean useSuffixPatternMatch = true;@@ -274,6 +278,18 @@ protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {}}+@Override+public RequestMatchResult match(HttpServletRequest request, String pattern) {+RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build();+RequestMappingInfo matchingInfo = info.getMatchingCondition(request);+if (matchingInfo == null) {+return null;+}+Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns();+String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);+return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher());+}+@Overrideprotected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {HandlerMethod handlerMethod = createHandlerMethod(handler, method);
spring-webmvc/src/main/java/org/springframework/web/servlet/support/HandlerMappingIntrospector.java+192 −0
@@ -0,0 +1,192 @@+/*+* 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+*+* 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.web.servlet.support;++import java.io.IOException;+import java.util.ArrayList;+import java.util.List;+import java.util.Map;+import java.util.Properties;+import javax.servlet.http.HttpServletRequest;+import javax.servlet.http.HttpServletRequestWrapper;++import org.springframework.beans.factory.BeanFactoryUtils;+import org.springframework.context.ApplicationContext;+import org.springframework.core.annotation.AnnotationAwareOrderComparator;+import org.springframework.core.io.ClassPathResource;+import org.springframework.core.io.Resource;+import org.springframework.core.io.support.PropertiesLoaderUtils;+import org.springframework.util.ClassUtils;+import org.springframework.util.StringUtils;+import org.springframework.web.cors.CorsConfiguration;+import org.springframework.web.cors.CorsConfigurationSource;+import org.springframework.web.servlet.DispatcherServlet;+import org.springframework.web.servlet.HandlerExecutionChain;+import org.springframework.web.servlet.HandlerInterceptor;+import org.springframework.web.servlet.HandlerMapping;++/**+* Helper class to get information from the {@code HandlerMapping} that would+* serve a specific request.+*+* <p>Provides the following methods:+* <ul>+* <li>{@link #getMatchableHandlerMapping} -- obtain a {@code HandlerMapping}+* to check request-matching criteria against.+* <li>{@link #getCorsConfiguration} -- obtain the CORS configuration for the+* request.+* </ul>+*+* @author Rossen Stoyanchev+* @since 4.3+*/+public class HandlerMappingIntrospector implements CorsConfigurationSource {++private final List<HandlerMapping> handlerMappings;+++/**+* Constructor that detects the configured {@code HandlerMapping}s in the+* given {@code ApplicationContext} or falling back on+* "DispatcherServlet.properties" like the {@code DispatcherServlet}.+*/+public HandlerMappingIntrospector(ApplicationContext context) {+this.handlerMappings = initHandlerMappings(context);+}+++private static List<HandlerMapping> initHandlerMappings(ApplicationContext context) {++Map<String, HandlerMapping> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(+context, HandlerMapping.class, true, false);++if (!beans.isEmpty()) {+List<HandlerMapping> mappings = new ArrayList<HandlerMapping>(beans.values());+AnnotationAwareOrderComparator.sort(mappings);+return mappings;+}++return initDefaultHandlerMappings(context);+}++private static List<HandlerMapping> initDefaultHandlerMappings(ApplicationContext context) {+Properties props;+String path = "DispatcherServlet.properties";+try {+Resource resource = new ClassPathResource(path, DispatcherServlet.class);+props = PropertiesLoaderUtils.loadProperties(resource);+}+catch (IOException ex) {+throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage());+}++String value = props.getProperty(HandlerMapping.class.getName());+String[] names = StringUtils.commaDelimitedListToStringArray(value);+List<HandlerMapping> result = new ArrayList<HandlerMapping>(names.length);+for (String name : names) {+try {+Class<?> clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader());+Object mapping = context.getAutowireCapableBeanFactory().createBean(clazz);+result.add((HandlerMapping) mapping);+}+catch (ClassNotFoundException ex) {+throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]");+}+}+return result;+}+++/**+* Return the configured HandlerMapping's.+*/+public List<HandlerMapping> getHandlerMappings() {+return this.handlerMappings;+}+++/**+* Find the {@link HandlerMapping} that would handle the given request and+* return it as a {@link MatchableHandlerMapping} that can be used to+* test request-matching criteria. If the matching HandlerMapping is not an+* instance of {@link MatchableHandlerMapping}, an IllegalStateException is+* raised.+*+* @param request the current request+* @return the resolved matcher, or {@code null}+* @throws Exception if any of the HandlerMapping's raise an exception+*/+public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {+HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request);+for (HandlerMapping handlerMapping : this.handlerMappings) {+Object handler = handlerMapping.getHandler(wrapper);+if (handler == null) {+continue;+}+if (handlerMapping instanceof MatchableHandlerMapping) {+return ((MatchableHandlerMapping) handlerMapping);+}+throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping");+}+return null;+}++@Override+public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {+HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request);+for (HandlerMapping handlerMapping : this.handlerMappings) {+HandlerExecutionChain handler = null;+try {+handler = handlerMapping.getHandler(wrapper);+}+catch (Exception ex) {+// Ignore+}+if (handler == null) {+continue;+}+if (handler.getInterceptors() != null) {+for (HandlerInterceptor interceptor : handler.getInterceptors()) {+if (interceptor instanceof CorsConfigurationSource) {+return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper);+}+}+}+if (handler.getHandler() instanceof CorsConfigurationSource) {+return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper);+}+}+return null;+}+++/**+* Request wrapper that ignores request attribute changes.+*/+private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper {+++private RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) {+super(request);+}++@Override+public void setAttribute(String name, Object value) {+// Ignore attribute change+}+}++}
spring-webmvc/src/main/java/org/springframework/web/servlet/support/MatchableHandlerMapping.java+41 −0
@@ -0,0 +1,41 @@+/*+* 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+*+* 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.web.servlet.support;++import javax.servlet.http.HttpServletRequest;++import org.springframework.web.servlet.HandlerMapping;++/**+* Additional interface that a {@link HandlerMapping} can implement to expose+* a request matching API aligned with its internal request matching+* configuration and implementation.+*+* @author Rossen Stoyanchev+* @since 4.3+* @see HandlerMappingIntrospector+*/+public interface MatchableHandlerMapping {++/**+* Whether the given request matches the request criteria.+* @param request the current request+* @param pattern the pattern to match+* @return the result from request matching or {@code null}+*/+RequestMatchResult match(HttpServletRequest request, String pattern);++}
spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestMatchResult.java+77 −0
@@ -0,0 +1,77 @@+/*+* 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+*+* 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.web.servlet.support;++import java.util.Collections;+import java.util.Map;++import org.springframework.util.Assert;+import org.springframework.util.PathMatcher;++/**+* Container for the result from request pattern matching via+* {@link MatchableHandlerMapping} with a method to further extract URI template+* variables from the pattern.+*+* @author Rossen Stoyanchev+* @since 4.3+*/+public class RequestMatchResult {++private final String matchingPattern;++private final String lookupPath;++private final PathMatcher pathMatcher;+++/**+* Create an instance with a matching pattern.+* @param matchingPattern the matching pattern, possibly not the same as the+* input pattern, e.g. inputPattern="/foo" and matchingPattern="/foo/".+* @param lookupPath the lookup path extracted from the request+* @param pathMatcher the PathMatcher used+*/+public RequestMatchResult(String matchingPattern, String lookupPath, PathMatcher pathMatcher) {+Assert.hasText(matchingPattern, "'matchingPattern' is required");+Assert.hasText(lookupPath, "'lookupPath' is required");+Assert.notNull(pathMatcher, "'pathMatcher' is required");+this.matchingPattern = matchingPattern;+this.lookupPath = lookupPath;+this.pathMatcher = pathMatcher;+}+++/**+* Whether the pattern was matched to the request.+*/+public boolean isMatch() {+return (this.matchingPattern != null);+}++/**+* Extract URI template variables from the matching pattern as defined in+* {@link PathMatcher#extractUriTemplateVariables}.+* @return a map with URI template variables+*/+public Map<String, String> extractUriTemplateVariables() {+if (!isMatch()) {+return Collections.<String, String>emptyMap();+}+return this.pathMatcher.extractUriTemplateVariables(this.matchingPattern, this.lookupPath);+}++}
spring-webmvc/src/test/java/org/springframework/web/servlet/support/HandlerMappingIntrospectorTests.java+190 −0
@@ -0,0 +1,190 @@+/*+* 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+*+* 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.web.servlet.support;++import java.util.Arrays;+import java.util.Collections;+import java.util.List;+import javax.servlet.http.HttpServletRequest;++import org.junit.Test;++import org.springframework.beans.MutablePropertyValues;+import org.springframework.context.annotation.Bean;+import org.springframework.context.annotation.Configuration;+import org.springframework.http.HttpHeaders;+import org.springframework.mock.web.test.MockHttpServletRequest;+import org.springframework.stereotype.Controller;+import org.springframework.web.bind.annotation.CrossOrigin;+import org.springframework.web.bind.annotation.PostMapping;+import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;+import org.springframework.web.context.support.StaticWebApplicationContext;+import org.springframework.web.cors.CorsConfiguration;+import org.springframework.web.servlet.HandlerExecutionChain;+import org.springframework.web.servlet.HandlerMapping;+import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;+import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;+import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping;+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertNotNull;+import static org.junit.Assert.assertNull;+import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE;++/**+* Unit tests for {@link HandlerMappingIntrospector}.+* @author Rossen Stoyanchev+*/+public class HandlerMappingIntrospectorTests {++@Test+public void detectHandlerMappings() throws Exception {+StaticWebApplicationContext cxt = new StaticWebApplicationContext();+cxt.registerSingleton("hmA", SimpleUrlHandlerMapping.class);+cxt.registerSingleton("hmB", SimpleUrlHandlerMapping.class);+cxt.registerSingleton("hmC", SimpleUrlHandlerMapping.class);+cxt.refresh();++List<?> expected = Arrays.asList(cxt.getBean("hmA"), cxt.getBean("hmB"), cxt.getBean("hmC"));+List<HandlerMapping> actual = new HandlerMappingIntrospector(cxt).getHandlerMappings();++assertEquals(expected, actual);+}++@Test+public void detectHandlerMappingsOrdered() throws Exception {+StaticWebApplicationContext cxt = new StaticWebApplicationContext();+MutablePropertyValues pvs = new MutablePropertyValues(Collections.singletonMap("order", "3"));+cxt.registerSingleton("hmA", SimpleUrlHandlerMapping.class, pvs);+pvs = new MutablePropertyValues(Collections.singletonMap("order", "2"));+cxt.registerSingleton("hmB", SimpleUrlHandlerMapping.class, pvs);+pvs = new MutablePropertyValues(Collections.singletonMap("order", "1"));+cxt.registerSingleton("hmC", SimpleUrlHandlerMapping.class, pvs);+cxt.refresh();++List<?> expected = Arrays.asList(cxt.getBean("hmC"), cxt.getBean("hmB"), cxt.getBean("hmA"));+List<HandlerMapping> actual = new HandlerMappingIntrospector(cxt).getHandlerMappings();++assertEquals(expected, actual);+}++@Test @SuppressWarnings("deprecation")+public void defaultHandlerMappings() throws Exception {+StaticWebApplicationContext cxt = new StaticWebApplicationContext();+cxt.refresh();++List<HandlerMapping> actual = new HandlerMappingIntrospector(cxt).getHandlerMappings();+assertEquals(2, actual.size());+assertEquals(BeanNameUrlHandlerMapping.class, actual.get(0).getClass());+assertEquals(DefaultAnnotationHandlerMapping.class, actual.get(1).getClass());+}++@Test+public void getMatchable() throws Exception {++MutablePropertyValues pvs = new MutablePropertyValues(+Collections.singletonMap("urlMap",+Collections.singletonMap("/path", new Object())));++StaticWebApplicationContext cxt = new StaticWebApplicationContext();+cxt.registerSingleton("hm", SimpleUrlHandlerMapping.class, pvs);+cxt.refresh();++MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");+MatchableHandlerMapping hm = new HandlerMappingIntrospector(cxt).getMatchableHandlerMapping(request);++assertEquals(cxt.getBean("hm"), hm);+assertNull("Attributes changes not ignored", request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE));+}++@Test(expected = IllegalStateException.class)+public void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() throws Exception {+StaticWebApplicationContext cxt = new StaticWebApplicationContext();+cxt.registerSingleton("hm1", TestHandlerMapping.class);+cxt.refresh();++MockHttpServletRequest request = new MockHttpServletRequest();+new HandlerMappingIntrospector(cxt).getMatchableHandlerMapping(request);+}++@Test+public void getCorsConfigurationPreFlight() throws Exception {+AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext();+cxt.register(TestConfig.class);+cxt.refresh();++// PRE-FLIGHT++MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path");+request.addHeader("Origin", "http://localhost:9000");+request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST");+CorsConfiguration corsConfig = new HandlerMappingIntrospector(cxt).getCorsConfiguration(request);++assertNotNull(corsConfig);+assertEquals(Collections.singletonList("http://localhost:9000"), corsConfig.getAllowedOrigins());+assertEquals(Collections.singletonList("POST"), corsConfig.getAllowedMethods());+}++@Test+public void getCorsConfigurationActual() throws Exception {+AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext();+cxt.register(TestConfig.class);+cxt.refresh();++MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path");+request.addHeader("Origin", "http://localhost:9000");+CorsConfiguration corsConfig = new HandlerMappingIntrospector(cxt).getCorsConfiguration(request);++assertNotNull(corsConfig);+assertEquals(Collections.singletonList("http://localhost:9000"), corsConfig.getAllowedOrigins());+assertEquals(Collections.singletonList("POST"), corsConfig.getAllowedMethods());+}+++private static class TestHandlerMapping implements HandlerMapping {++@Override+public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {+return new HandlerExecutionChain(new Object());+}+}++@Configuration @SuppressWarnings({"WeakerAccess", "unused"})+static class TestConfig {++@Bean+public RequestMappingHandlerMapping handlerMapping() {+return new RequestMappingHandlerMapping();+}++@Bean+public TestController testController() {+return new TestController();+}++}++@CrossOrigin("http://localhost:9000")+@Controller+private static class TestController {++@PostMapping("/path")+public void handle() {+}+}++}
Add MvcRequestMatcher
config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java+5 −4
@@ -25,6 +25,7 @@import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;+import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.util.Assert;import org.springframework.web.filter.DelegatingFilterProxy;@@ -57,7 +58,7 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuiprivate final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>>();private final List<SecurityConfigurer<O, B>> configurersAddedInInitializing = new ArrayList<SecurityConfigurer<O, B>>();-private final Map<Class<Object>, Object> sharedObjects = new HashMap<Class<Object>, Object>();+private final Map<Class<? extends Object>, Object> sharedObjects = new HashMap<Class<? extends Object>, Object>();private final boolean allowConfigurersOfSameType;@@ -155,7 +156,7 @@ public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Excepti*/@SuppressWarnings("unchecked")public <C> void setSharedObject(Class<C> sharedType, C object) {-this.sharedObjects.put((Class<Object>) sharedType, object);+this.sharedObjects.put(sharedType, object);}/**@@ -173,7 +174,7 @@ public <C> C getSharedObject(Class<C> sharedType) {* Gets the shared objects* @return*/-public Map<Class<Object>, Object> getSharedObjects() {+public Map<Class<? extends Object>, Object> getSharedObjects() {return Collections.unmodifiableMap(this.sharedObjects);}@@ -300,7 +301,7 @@ public O objectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {* @return the possibly modified Object to use*/protected <P> P postProcess(P object) {-return (P) this.objectPostProcessor.postProcess(object);+return this.objectPostProcessor.postProcess(object);}/**
config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java+60 −0
@@ -19,12 +19,15 @@import java.util.Arrays;import java.util.List;+import org.springframework.context.ApplicationContext;import org.springframework.http.HttpMethod;import org.springframework.security.config.annotation.web.configurers.AbstractConfigAttributeRequestMatcherRegistry;+import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;import org.springframework.security.web.util.matcher.AntPathRequestMatcher;import org.springframework.security.web.util.matcher.AnyRequestMatcher;import org.springframework.security.web.util.matcher.RegexRequestMatcher;import org.springframework.security.web.util.matcher.RequestMatcher;+import org.springframework.web.servlet.handler.HandlerMappingIntrospector;/*** A base class for registering {@link RequestMatcher}'s. For example, it might allow for@@ -39,6 +42,12 @@public abstract class AbstractRequestMatcherRegistry<C> {private static final RequestMatcher ANY_REQUEST = AnyRequestMatcher.INSTANCE;+private ApplicationContext context;++protected final void setApplicationContext(ApplicationContext context) {+this.context = context;+}+/*** Maps any request.*@@ -92,6 +101,57 @@ public C antMatchers(String... antPatterns) {return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));}+/**+* <p>+* Maps an {@link MvcRequestMatcher} that does not care which {@link HttpMethod} is+* used. This matcher will use the same rules that Spring MVC uses for matching. For+* example, often times a mapping of the path "/path" will match on "/path", "/path/",+* "/path.html", etc.+* </p>+* <p>+* If the current request will not be processed by Spring MVC, a reasonable default+* using the pattern as a ant pattern will be used.+* </p>+*+* @param mvcPatterns the patterns to match on. The rules for matching are defined by+* Spring MVC+* @return the object that is chained after creating the {@link RequestMatcher}.+*/+public C mvcMatchers(String... mvcPatterns) {+return mvcMatchers(null, mvcPatterns);+}++/**+* <p>+* Maps an {@link MvcRequestMatcher} that also specifies a specific {@link HttpMethod}+* to match on. This matcher will use the same rules that Spring MVC uses for+* matching. For example, often times a mapping of the path "/path" will match on+* "/path", "/path/", "/path.html", etc.+* </p>+* <p>+* If the current request will not be processed by Spring MVC, a reasonable default+* using the pattern as a ant pattern will be used.+* </p>+*+* @param method the HTTP method to match on+* @param mvcPatterns the patterns to match on. The rules for matching are defined by+* Spring MVC+* @return the object that is chained after creating the {@link RequestMatcher}.+*/+public C mvcMatchers(HttpMethod method, String... mvcPatterns) {+HandlerMappingIntrospector introspector = new HandlerMappingIntrospector(+this.context);+List<RequestMatcher> matchers = new ArrayList<RequestMatcher>(mvcPatterns.length);+for (String mvcPattern : mvcPatterns) {+MvcRequestMatcher matcher = new MvcRequestMatcher(introspector, mvcPattern);+if (method != null) {+matcher.setMethod(method);+}+matchers.add(matcher);+}+return chainRequestMatchers(matchers);+}+/*** Maps a {@link List} of* {@link org.springframework.security.web.util.matcher.RegexRequestMatcher}
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java+30 −9
@@ -23,6 +23,7 @@import javax.servlet.Filter;import javax.servlet.http.HttpServletRequest;+import org.springframework.context.ApplicationContext;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.AuthenticationProvider;import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;@@ -62,6 +63,7 @@import org.springframework.security.web.DefaultSecurityFilterChain;import org.springframework.security.web.PortMapper;import org.springframework.security.web.PortMapperImpl;+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;import org.springframework.security.web.session.HttpSessionEventPublisher;import org.springframework.security.web.util.matcher.AntPathRequestMatcher;@@ -113,7 +115,7 @@ public final class HttpSecurity extendsAbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>implements SecurityBuilder<DefaultSecurityFilterChain>,HttpSecurityBuilder<HttpSecurity> {-private final RequestMatcherConfigurer requestMatcherConfigurer = new RequestMatcherConfigurer();+private final RequestMatcherConfigurer requestMatcherConfigurer;private List<Filter> filters = new ArrayList<Filter>();private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;private FilterComparator comparator = new FilterComparator();@@ -126,15 +128,24 @@ public final class HttpSecurity extends* @param sharedObjects the shared Objects to initialize the {@link HttpSecurity} with* @see WebSecurityConfiguration*/+@SuppressWarnings("unchecked")public HttpSecurity(ObjectPostProcessor<Object> objectPostProcessor,AuthenticationManagerBuilder authenticationBuilder,-Map<Class<Object>, Object> sharedObjects) {+Map<Class<? extends Object>, Object> sharedObjects) {super(objectPostProcessor);Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null");setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder);-for (Map.Entry<Class<Object>, Object> entry : sharedObjects.entrySet()) {-setSharedObject(entry.getKey(), entry.getValue());+for (Map.Entry<Class<? extends Object>, Object> entry : sharedObjects+.entrySet()) {+setSharedObject((Class<Object>) entry.getKey(), entry.getValue());}+ApplicationContext context = (ApplicationContext) sharedObjects+.get(ApplicationContext.class);+this.requestMatcherConfigurer = new RequestMatcherConfigurer(context);+}++private ApplicationContext getContext() {+return getSharedObject(ApplicationContext.class);}/**@@ -634,7 +645,8 @@ public RememberMeConfigurer<HttpSecurity> rememberMe() throws Exception {*/public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests()throws Exception {-return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>())+ApplicationContext context = getContext();+return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>(context)).getRegistry();}@@ -710,7 +722,8 @@ public ServletApiConfigurer<HttpSecurity> servletApi() throws Exception {* @throws Exception*/public CsrfConfigurer<HttpSecurity> csrf() throws Exception {-return getOrApply(new CsrfConfigurer<HttpSecurity>());+ApplicationContext context = getContext();+return getOrApply(new CsrfConfigurer<HttpSecurity>(context));}/**@@ -917,7 +930,9 @@ public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {*/public ChannelSecurityConfigurer<HttpSecurity>.ChannelRequestMatcherRegistry requiresChannel()throws Exception {-return getOrApply(new ChannelSecurityConfigurer<HttpSecurity>()).getRegistry();+ApplicationContext context = getContext();+return getOrApply(new ChannelSecurityConfigurer<HttpSecurity>(context))+.getRegistry();}/**@@ -1241,8 +1256,16 @@ public HttpSecurity regexMatcher(String pattern) {*/public final class RequestMatcherConfigurer extendsAbstractRequestMatcherRegistry<RequestMatcherConfigurer> {+private List<RequestMatcher> matchers = new ArrayList<RequestMatcher>();+/**+* @param context+*/+private RequestMatcherConfigurer(ApplicationContext context) {+setApplicationContext(context);+}+protected RequestMatcherConfigurer chainRequestMatchers(List<RequestMatcher> requestMatchers) {matchers.addAll(requestMatchers);@@ -1259,8 +1282,6 @@ public HttpSecurity and() {return HttpSecurity.this;}-private RequestMatcherConfigurer() {-}}/**
config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java+10 −5
@@ -23,6 +23,7 @@import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;+import org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;@@ -80,7 +81,7 @@ public final class WebSecurity extendsprivate final List<SecurityBuilder<? extends SecurityFilterChain>> securityFilterChainBuilders = new ArrayList<SecurityBuilder<? extends SecurityFilterChain>>();-private final IgnoredRequestConfigurer ignoredRequestRegistry = new IgnoredRequestConfigurer();+private IgnoredRequestConfigurer ignoredRequestRegistry;private FilterSecurityInterceptor filterSecurityInterceptor;@@ -316,6 +317,10 @@ protected Filter performBuild() throws Exception {public final class IgnoredRequestConfigurer extendsAbstractRequestMatcherRegistry<IgnoredRequestConfigurer> {+private IgnoredRequestConfigurer(ApplicationContext context) {+setApplicationContext(context);+}+@Overrideprotected IgnoredRequestConfigurer chainRequestMatchers(List<RequestMatcher> requestMatchers) {@@ -329,13 +334,13 @@ protected IgnoredRequestConfigurer chainRequestMatchers(public WebSecurity and() {return WebSecurity.this;}--private IgnoredRequestConfigurer() {-}}+@Overridepublic void setApplicationContext(ApplicationContext applicationContext)throws BeansException {-defaultWebSecurityExpressionHandler.setApplicationContext(applicationContext);+this.defaultWebSecurityExpressionHandler+.setApplicationContext(applicationContext);+this.ignoredRequestRegistry = new IgnoredRequestConfigurer(applicationContext);}}
config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java+8 −0
@@ -329,6 +329,14 @@ protected void configure(HttpSecurity http) throws Exception {}// @formatter:on+/**+* Gets the ApplicationContext+* @return the context+*/+protected final ApplicationContext getApplicationContext() {+return this.context;+}+@Autowiredpublic void setApplicationContext(ApplicationContext context) {this.context = context;
config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java+8 −5
@@ -20,6 +20,7 @@import java.util.LinkedHashMap;import java.util.List;+import org.springframework.context.ApplicationContext;import org.springframework.security.access.ConfigAttribute;import org.springframework.security.access.SecurityConfig;import org.springframework.security.config.annotation.ObjectPostProcessor;@@ -80,13 +81,14 @@ public final class ChannelSecurityConfigurer<H extends HttpSecurityBuilder<H>> eprivate LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>();private List<ChannelProcessor> channelProcessors;-private final ChannelRequestMatcherRegistry REGISTRY = new ChannelRequestMatcherRegistry();+private final ChannelRequestMatcherRegistry REGISTRY;/*** Creates a new instance* @see HttpSecurity#requiresChannel()*/-public ChannelSecurityConfigurer() {+public ChannelSecurityConfigurer(ApplicationContext context) {+this.REGISTRY = new ChannelRequestMatcherRegistry(context);}public ChannelRequestMatcherRegistry getRegistry() {@@ -146,6 +148,10 @@ private ChannelRequestMatcherRegistry addAttribute(String attribute,public final class ChannelRequestMatcherRegistry extendsAbstractConfigAttributeRequestMatcherRegistry<RequiresChannelUrl> {+private ChannelRequestMatcherRegistry(ApplicationContext context) {+setApplicationContext(context);+}+@Overrideprotected RequiresChannelUrl chainRequestMatchersInternal(List<RequestMatcher> requestMatchers) {@@ -185,9 +191,6 @@ public ChannelRequestMatcherRegistry channelProcessors(public H and() {return ChannelSecurityConfigurer.this.and();}--private ChannelRequestMatcherRegistry() {-}}public final class RequiresChannelUrl {
config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java+13 −2
@@ -21,6 +21,7 @@import javax.servlet.http.HttpServletRequest;+import org.springframework.context.ApplicationContext;import org.springframework.security.access.AccessDeniedException;import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;import org.springframework.security.config.annotation.web.HttpSecurityBuilder;@@ -78,12 +79,14 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>new HttpSessionCsrfTokenRepository());private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;private List<RequestMatcher> ignoredCsrfProtectionMatchers = new ArrayList<RequestMatcher>();+private final ApplicationContext context;/*** Creates a new instance* @see HttpSecurity#csrf()*/-public CsrfConfigurer() {+public CsrfConfigurer(ApplicationContext context) {+this.context = context;}/**@@ -141,7 +144,8 @@ public CsrfConfigurer<H> requireCsrfProtectionMatcher(* @since 4.0*/public CsrfConfigurer<H> ignoringAntMatchers(String... antPatterns) {-return new IgnoreCsrfProtectionRegistry().antMatchers(antPatterns).and();+return new IgnoreCsrfProtectionRegistry(this.context).antMatchers(antPatterns)+.and();}@SuppressWarnings("unchecked")@@ -265,6 +269,13 @@ private AccessDeniedHandler createAccessDeniedHandler(H http) {private class IgnoreCsrfProtectionRegistryextends AbstractRequestMatcherRegistry<IgnoreCsrfProtectionRegistry> {+/**+* @param context+*/+private IgnoreCsrfProtectionRegistry(ApplicationContext context) {+setApplicationContext(context);+}+public CsrfConfigurer<H> and() {return CsrfConfigurer.this;}
config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java+10 −2
@@ -84,7 +84,7 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBuprivate static final String fullyAuthenticated = "fullyAuthenticated";private static final String rememberMe = "rememberMe";-private final ExpressionInterceptUrlRegistry REGISTRY = new ExpressionInterceptUrlRegistry();+private final ExpressionInterceptUrlRegistry REGISTRY;private SecurityExpressionHandler<FilterInvocation> expressionHandler;@@ -92,7 +92,8 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBu* Creates a new instance* @see HttpSecurity#authorizeRequests()*/-public ExpressionUrlAuthorizationConfigurer() {+public ExpressionUrlAuthorizationConfigurer(ApplicationContext context) {+this.REGISTRY = new ExpressionInterceptUrlRegistry(context);}public ExpressionInterceptUrlRegistry getRegistry() {@@ -103,6 +104,13 @@ public class ExpressionInterceptUrlRegistryextendsExpressionUrlAuthorizationConfigurer<H>.AbstractInterceptUrlRegistry<ExpressionInterceptUrlRegistry, AuthorizedUrl> {+/**+* @param context+*/+private ExpressionInterceptUrlRegistry(ApplicationContext context) {+setApplicationContext(context);+}+@Overrideprotected final AuthorizedUrl chainRequestMatchersInternal(List<RequestMatcher> requestMatchers) {
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2016-5007
- WEBhttps://github.com/spring-projects/spring-security/issues/3964
- WEBhttps://github.com/spring-projects/spring-framework/commit/a30ab30e4e9ae021fdda04e9abfc228476b846b5
- WEBhttps://github.com/spring-projects/spring-security/commit/e4c13e3c0ee7f06f59d3b43ca6734215ad7d8974
- ADVISORYhttps://github.com/advisories/GHSA-8crv-49fr-2h6j
- WEBhttps://pivotal.io/security/cve-2016-5007
- WEBhttps://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html
- WEBhttp://www.oracle.com/technetwork/security-advisory/cpuapr2018-3678067.html
- WEBhttp://www.securityfocus.com/bid/91687