Describe the bug We have a method that checks if a request refers to a given path using

    private Optional<Method> getEndpoint(HttpServletRequest request) {
        PathPatternParser pathParser = new PathPatternParser();
        PathPattern pathPattern = pathParser
                .parse(endpointProperties.getEndpointPrefix()
                        + EndpointController.ENDPOINT_METHODS);
        RequestPath requestPath = ServletRequestPathUtils
                .parseAndCache(request);
...

This seems to work fine in most cases but in some cases, like when doing a dummy POST to a restricted URL, the method is called with a HttpServletRequest of type org.springframework.security.web.FilterInvocation$DummyRequest. When you call ServletRequestPathUtils.parseAndCache on this request, it fails with

Apr 05, 2022 2:38:56 PM org.apache.catalina.core.StandardHostValve custom
SEVERE: Exception Processing ErrorPage[errorCode=0, location=/error]
java.lang.UnsupportedOperationException: public abstract void javax.servlet.ServletRequest.setAttribute(java.lang.String,java.lang.Object) is not supported
        at org.springframework.security.web.FilterInvocation$UnsupportedOperationExceptionInvocationHandler.invoke(FilterInvocation.java:326)
        at com.sun.proxy.$Proxy110.setAttribute(Unknown Source)
        at javax.servlet.ServletRequestWrapper.setAttribute(ServletRequestWrapper.java:259)
        at org.springframework.web.util.ServletRequestPathUtils.parseAndCache(ServletRequestPathUtils.java:67)
        at dev.hilla.EndpointUtil.getEndpoint(EndpointUtil.java:70)
        at dev.hilla.EndpointUtil.isAnonymousEndpoint(EndpointUtil.java:99)
        at com.vaadin.flow.spring.security.RequestUtil.isAnonymousEndpoint(RequestUtil.java:107)
        at org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource.getAttributes(DefaultFilterInvocationSecurityMetadataSource.java:84)
        at org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator.isAllowed(DefaultWebInvocationPrivilegeEvaluator.java:92)
        at org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator.isAllowed(DefaultWebInvocationPrivilegeEvaluator.java:67)
        at org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.isAllowed(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java:76)
        at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.isAllowed(ErrorPageSecurityFilter.java:88)
        at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.doFilter(ErrorPageSecurityFilter.java:76)
        at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.doFilter(ErrorPageSecurityFilter.java:70)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:327)
        at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:115)
        at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:122)
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:116)

as parseAndCache is trying to cache the result as a request attribute.

To Reproduce Presumably, untested, only extracted from Vaadin code

http.exceptionHandling().accessDeniedHandler(createAccessDeniedHandler())

...

private AccessDeniedHandler createAccessDeniedHandler() {
    final LinkedHashMap<RequestMatcher, AccessDeniedHandler> matcherHandlers = new LinkedHashMap<>();
    matcherHandlers.put(request -> {ServletRequestPathUtils.parseAndCache(request); return false;}, 
                new AccessDeniedHandlerImpl());
    return new RequestMatcherDelegatingAccessDeniedHandler(matcherHandlers,
                new AccessDeniedHandlerImpl());
}

Expected behavior No exception is thrown. The checker method can conclude this is not an endpoint request.

Comment From: rdanilin

We could see a strange issue with DummyRequest (which may not relate to the original issue) after upgrading to v5.6.2. The version 5.6.0 works fine.

Exception Processing ErrorPage[errorCode=0, location=/error]
java.lang.NullPointerException: null
    at java.base/java.util.Collections$3.<init>(Collections.java:5260)
    at java.base/java.util.Collections.enumeration(Collections.java:5259)
    at org.springframework.security.web.FilterInvocation$DummyRequest.getHeaders(FilterInvocation.java:260)
    at org.springframework.web.context.request.ServletWebRequest.getHeaderValues(ServletWebRequest.java:135)
    at org.springframework.web.accept.HeaderContentNegotiationStrategy.resolveMediaTypes(HeaderContentNegotiationStrategy.java:46)
    at org.springframework.security.web.util.matcher.MediaTypeRequestMatcher.matches(MediaTypeRequestMatcher.java:203)
    at org.springframework.security.web.util.matcher.OrRequestMatcher.matches(OrRequestMatcher.java:58)
    at org.springframework.security.web.DefaultSecurityFilterChain.matches(DefaultSecurityFilterChain.java:72)
    at org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.getDelegate(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java:120)
    at org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.isAllowed(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java:71)
    at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.isAllowed(ErrorPageSecurityFilter.java:75)
    at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.doFilter(ErrorPageSecurityFilter.java:65)
    at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.doFilter(ErrorPageSecurityFilter.java:60)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)

Comment From: Artur-

Here is a reduce reproduction case:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final class FailingMatcher implements RequestMatcher {
        @Override
        public boolean matches(HttpServletRequest request) {
            System.out.println("Request to " + ServletRequestPathUtils.parseAndCache(request));
            return true;
        }
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.requestMatcher(new FailingMatcher());
    }

}

Error triggered by

curl localhost:8080

Also in https://github.com/Artur-/spring-security-test

Comment From: Artur-

This indeed does not happen with Spring Boot 2.6.3, i.e. Spring Security 5.6.1 but it happens with Spring Boot 2.6.4+ i.e. Spring Security 5.6.2+

Comment From: Artur-

Bisected. Caused by 994e93741bfbe42b04befdeb4a4dcdd8021452f6 by @marcusdacoregio

Comment From: marcusdacoregio

Hi, thanks for the report.

Prior to 5.6.2, the WebInvocationPrivilegeEvaluator was not aware of multiple filter chains, it was also not aware of the requestMatcher for the SecurityFilterChain at all. https://github.com/spring-projects/spring-security/commit/994e93741bfbe42b04befdeb4a4dcdd8021452f6 fixed this behavior by taking into consideration the requestMatcher for each SecurityFilterChain configured.

In Spring Boot 2.6.0, the ErrorPageSecurityFilter was introduced to perform authorization checks on DispatcherType.ERROR, and it uses the WebInvocationPrivilegeEvaluator API to perform those checks. The WebInvocationPrivilegeEvaluator is not intended to use in scenarios where users need anything but the URL from the request, therefore its usage does not really fit into the ErrorPageSecurityFilter, see https://github.com/spring-projects/spring-security/issues/10919 and https://github.com/spring-projects/spring-security/issues/11092.

That said, what I recommend folks do is to consider removing the ErrorPageSecurityFilter:

@Bean
public static BeanFactoryPostProcessor removeErrorSecurityFilter() {
    return beanFactory -> ((DefaultListableBeanFactory) beanFactory).removeBeanDefinition("errorPageSecurityInterceptor");
}

And, if you want the authorization checks for all DispatcherTypes you can do:

http
    .authorizeRequests((requests) -> requests
        .filterSecurityInterceptorOncePerRequest(false)
        ...
    )
        ...

If you are using authorizeHttpRequests, once 5.7.0 is released, you can use shouldFilterAllDispatcherTypes, like so:

http
    .authorizeHttpRequests((requests) -> requests
        .shouldFilterAllDispatcherTypes(true)
        ...
    )
        ...

Note that in Spring Security 6.0, all the DispatcherTypes will be filtered by default, see https://github.com/spring-projects/spring-security/issues/11027.

Related: - https://github.com/spring-projects/spring-security/issues/10694 - https://github.com/spring-projects/spring-security/issues/10664