I'm creating this issue to group together alternatives to ErrorPageSecurityFilter instead of relying on the WebInvocationPrivilegeEvaluator interface.

Context: The recently introduced ErrorPageSecurityFilter is using the WebInvocationPrivilegeEvaluator API to decide if the request is authorized to access the Error page.

Unfortunately, the WebInvocationPrivilegeEvaluator interface was designed to work with only a few attributes from the request, like uri and method. In Spring Security, users can configure their authorization model using any attribute from the request if they want, therefore resulting in an exception (see https://github.com/spring-projects/spring-security/issues/10664):

http
                .authorizeRequests()
                .antMatchers("/error/**").hasIpAddress("127.0.0.1")

Or if they implement a custom AccessDecisionVoter and it uses anything else from the request object (see https://github.com/spring-projects/spring-security/issues/10694).

One of my proposal is the following: the ErrorPageSecurityFilter should not rely on the WebInvocationPrivilegeEvaluator to check for access permissions, instead, it should use a AuthorizationManager<HttpServletRequest> implementation that is built using the FilterSecurityInterceptor or the AuthorizationFilter from the SecurityFilterChain.

Draft:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(FilterChainProxy.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
class ErrorPageSecurityFilterConfiguration {

    @Bean
    @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
    FilterRegistrationBean<ErrorPageSecurityFilter> errorPageSecurityInterceptor(ApplicationContext context,
            @Qualifier(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) Filter filterChainProxy) {
        RequestMatcherDelegatingAuthorizationManager authorizationManager = createAuthorizationManager((FilterChainProxy) filterChainProxy);
        FilterRegistrationBean<ErrorPageSecurityFilter> registration = new FilterRegistrationBean<>(
                new ErrorPageSecurityFilter(context, authorizationManager));
        registration.setDispatcherTypes(EnumSet.of(DispatcherType.ERROR));
        return registration;
    }

    private RequestMatcherDelegatingAuthorizationManager createAuthorizationManager(FilterChainProxy filterChainProxy) {
        RequestMatcherDelegatingAuthorizationManager.Builder builder = new RequestMatcherDelegatingAuthorizationManager.Builder();
        for (SecurityFilterChain securityFilterChain : filterChainProxy.getFilterChains()) {
            for (Filter filter : securityFilterChain.getFilters()) {
                if (filter instanceof FilterSecurityInterceptor) {
                    FilterSecurityInterceptor filterSecurityInterceptor = (FilterSecurityInterceptor) filter;
                    AuthorizationManager<RequestAuthorizationContext> accessDecisionManagerAuthorizationManager = new AccessDecisionManagerAuthorizationManagerAdapter(
                            filterSecurityInterceptor.obtainSecurityMetadataSource(),
                            filterSecurityInterceptor.getAccessDecisionManager(),
                            filterSecurityInterceptor.isRejectPublicInvocations());
                    builder.add(securityFilterChain::matches, accessDecisionManagerAuthorizationManager);
                }
                if (filter instanceof AuthorizationFilter) {
                    AuthorizationManager<HttpServletRequest> authorizationManager = ((AuthorizationFilter) filter)
                            .getAuthorizationManager();
                    builder.add(securityFilterChain::matches,
                            new HttpServletRequestAuthorizationManagerAdapter(authorizationManager));
                }
            }
        }
        return builder.build();
    }

    static class HttpServletRequestAuthorizationManagerAdapter
            implements AuthorizationManager<RequestAuthorizationContext> {

        private final AuthorizationManager<HttpServletRequest> delegate;

        HttpServletRequestDelegatingAuthorizationManager(AuthorizationManager<HttpServletRequest> delegate) {
            this.delegate = delegate;
        }

        @Override
        public AuthorizationDecision check(Supplier<Authentication> authentication,
                RequestAuthorizationContext context) {
            return this.delegate.check(authentication, context.getRequest());
        }

    }

    static class AccessDecisionManagerAuthorizationManagerAdapter
            implements AuthorizationManager<RequestAuthorizationContext> {

        private final SecurityMetadataSource securityMetadataSource;

        private final AccessDecisionManager accessDecisionManager;

        private final boolean isRejectPublicInvocations;

        AccessDecisionManagerAuthorizationManager(SecurityMetadataSource securityMetadataSource,
                AccessDecisionManager accessDecisionManager, boolean isRejectPublicInvocations) {
            this.securityMetadataSource = securityMetadataSource;
            this.accessDecisionManager = accessDecisionManager;
            this.isRejectPublicInvocations = isRejectPublicInvocations;
        }

        @Override
        public AuthorizationDecision check(Supplier<Authentication> authenticationSupplier,
                RequestAuthorizationContext context) {
            HttpServletRequest request = context.getRequest();
            FilterInvocation filterInvocation = new FilterInvocation(request);
            Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(filterInvocation);
            if (attributes == null) {
                return new AuthorizationDecision(!this.isRejectPublicInvocations);
            }
            Authentication authentication = authenticationSupplier.get();
            if (authentication == null) {
                return new AuthorizationDecision(false);
            }
            try {
                this.accessDecisionManager.decide(authentication, filterInvocation, attributes);
                return new AuthorizationDecision(true);
            }
            catch (AccessDeniedException ex) {
                return new AuthorizationDecision(false);
            }
        }

    }

}
public class ErrorPageSecurityFilter implements Filter {

        ...

        private AuthorizationManager<HttpServletRequest> authorizationManager;

    public ErrorPageSecurityFilter(ApplicationContext context,
            AuthorizationManager<HttpServletRequest> authorizationManager) {
        this(context);
        this.authorizationManager = authorizationManager;
    }

        ...

        private boolean isAllowed(HttpServletRequest request, Integer errorCode) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (isUnauthenticated(authentication) && isNotAuthenticationError(errorCode)) {
            return true;
        }
        AuthorizationDecision decision = this.authorizationManager.check(() -> authentication, request);
        return decision == null && decision.isGranted();
    }
}

The AuthorizationManager was introduced in Spring Security 5.5 and supersedes AccessDecisionManager and AccessDecisionVoter, it is used by the AuthorizationFilter when specifying http.authorizeHttpRequests().

Comment From: marcusdacoregio

Closing this in favor of https://github.com/spring-projects/spring-security/issues/10919

Comment From: mbhave

@marcusdacoregio Did you mean to link to another issue? This links to this issue itself.

Comment From: marcusdacoregio

Thanks, I've updated the comment