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