Describe the bug When upgrading an internal library to Spring Security 5.8, we encountered a StackOverflowError caused by the interaction between AnonymousAuthenticationFilter and a custom logback TurboFilter implementation. Said filter uses SecurityContextHolder.getContext() to access the principal and checks whether it implements an interface to decide whether to force logging or not. It seems that due to https://github.com/spring-projects/spring-security/issues/11457 this causes an endless recursion by AnonymousAuthenticationFilter checking whether trace logging is enabled and the filter checking the security context.

To Reproduce Implement a logback TurboFilter accessing the security context and add it to the logback-spring.xml. Cause a logging event for an anonymous user or any other lazily initialized security context.

Expected behavior No StackOverflowError as with Spring Security 5.7 and before.

Sample https://github.com/schosin/anonymous-logback-stackoverflow This project uses Spring Boot 3.2, but the problem persists since 2.7 + Spring Security 5.8. The stacktrace for this sample project is different from our internal project. See below for the difference.

Stacktrace sample project at de.schosin.spring.security.anonymouslogbackstackoverflow.SecurityContextAccessingTurboFilter.decide(SecurityContextAccessingTurboFilter.java:15) at ch.qos.logback.classic.spi.TurboFilterList.getTurboFilterChainDecision(TurboFilterList.java:49) at ch.qos.logback.classic.LoggerContext.getTurboFilterChainDecision_0_3OrMore(LoggerContext.java:262) at ch.qos.logback.classic.Logger.filterAndLog_0_Or3Plus(Logger.java:375) at ch.qos.logback.classic.Logger.log(Logger.java:780) at org.apache.commons.logging.LogAdapter$Slf4jLocationAwareLog.trace(LogAdapter.java:480) at org.springframework.security.web.context.HttpSessionSecurityContextRepository.readSecurityContextFromSession(HttpSessionSecurityContextRepository.java:206) at org.springframework.security.web.context.HttpSessionSecurityContextRepository.lambda$loadDeferredContext$0(HttpSessionSecurityContextRepository.java:143) at org.springframework.security.web.context.SupplierDeferredSecurityContext.init(SupplierDeferredSecurityContext.java:67) at org.springframework.security.web.context.SupplierDeferredSecurityContext.get(SupplierDeferredSecurityContext.java:52) at org.springframework.security.web.context.SupplierDeferredSecurityContext.get(SupplierDeferredSecurityContext.java:33) at org.springframework.security.web.context.DelegatingSecurityContextRepository$DelegatingDeferredSecurityContext.get(DelegatingSecurityContextRepository.java:104) at org.springframework.security.web.context.DelegatingSecurityContextRepository$DelegatingDeferredSecurityContext.get(DelegatingSecurityContextRepository.java:91) at org.springframework.security.core.context.ThreadLocalSecurityContextHolderStrategy.lambda$setDeferredContext$2(ThreadLocalSecurityContextHolderStrategy.java:67) at org.springframework.security.core.context.ThreadLocalSecurityContextHolderStrategy.getContext(ThreadLocalSecurityContextHolderStrategy.java:43) at org.springframework.security.core.context.SecurityContextHolder.getContext(SecurityContextHolder.java:125) at de.schosin.spring.security.anonymouslogbackstackoverflow.SecurityContextAccessingTurboFilter.decide(SecurityContextAccessingTurboFilter.java:15)

Stacktrace Spring Boot 2.7.x + Spring Security 5.8.8 (internal library) at somepackage.LogTraceFilter.decide(LogTraceFilter.java:24) at ch.qos.logback.classic.spi.TurboFilterList.getTurboFilterChainDecision(TurboFilterList.java:60) at ch.qos.logback.classic.LoggerContext.getTurboFilterChainDecision_0_3OrMore(LoggerContext.java:269) at ch.qos.logback.classic.Logger.callTurboFilters(Logger.java:751) at ch.qos.logback.classic.Logger.isTraceEnabled(Logger.java:623) at ch.qos.logback.classic.Logger.isTraceEnabled(Logger.java:619) at org.apache.commons.logging.LogAdapter$Slf4jLog.isTraceEnabled(LogAdapter.java:315) at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.defaultWithAnonymous(AnonymousAuthenticationFilter.java:126) at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.lambda$defaultWithAnonymous$0(AnonymousAuthenticationFilter.java:107) at org.springframework.util.function.SingletonSupplier.get(SingletonSupplier.java:97) at org.springframework.security.core.context.InheritableThreadLocalSecurityContextHolderStrategy.lambda$setDeferredContext$2(InheritableThreadLocalSecurityContextHolderStrategy.java:66) at org.springframework.security.core.context.InheritableThreadLocalSecurityContextHolderStrategy.getContext(InheritableThreadLocalSecurityContextHolderStrategy.java:42) at org.springframework.security.core.context.SecurityContextHolder.getContext(SecurityContextHolder.java:125) at somepackage.LogTraceFilter.traceLoggingForCurrentUser(LogTraceFilter.java:38) at somepackage.LogTraceFilter.decide(LogTraceFilter.java:24)

Comment From: schosin

As a side note, the very same TurboFilter is being triggered by logging before Spring Security initialization. This causes SecurityContextHolder#initializeStrategy to initialize a ThreadLocalSecurityContextHolderStrategy that is picked up by PreFilterAuthorizationMethodInterceptor before our own configurations set the strategy to use.

That way PreAuthorizeAuthorizationManager has a reference to a SecurityContextHolderStrategy that will be discarded if any code calls setContextHolderStrategy/setStrategyName afterwards. As such, we had failing tests using @WithUserDetails as it populates the wrong SecurityContextHolderStrategy for any prePost checks.

This might be a order problem, but up until we now used the constructor of our @EnableWebSecurity/@EnableMethodSecurity(prePostEnabled = true) configuration to set the strategy, which worked just fine. As a workaround, we removed the explicit call to setStrategyName and call System.setProperty(SecurityContextHolder.SYSTEM_PROPERTY, SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); before SpringApplication.run(...).

This in addition to skipping "org.springframework.security" loggers in the filter seems to work as a workaround for now. But both feel like unfortunate changes since we did not have that issue with Spring Security 5.7.

Comment From: jzheaux

Thanks for reaching out, @schosin.

The trouble is that you're application is trying to consult the authentication while it's still being resolved. Since Spring Security 5.8, calling setAuthentication is deferred, meaning that consulting the security context holder before the authentication filters have completed their work can cause this issue.

You may want to extract the authentication lookup from the logger itself. For example, post-authentication, you could add the authentication to the MDC, then your logger can retrieve it from there like so:

http
    .addFilterBefore((request, response, chain) -> {
        MDC.put("principal", SecurityContextHolder.getContext().getAuthentication().getName());
        chain.doFilter(request, response);
    }, AuthorizationFilter.class)
// ...

@Override
public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {
    String name = MDC.get("principal");
    return FilterReply.NEUTRAL;
}

Or, if you need the full Authentication instance, you might use a custom ThreadLocal map of properties instead of the MDC.

Do you think that would help? If not, can you share more of your turbo filter's implementation and what you want to achieve?

Comment From: schosin

I think I can work with MDC/ThreadLocal if I populate it after authorization for each request. HttpSession would probably work as well. Since the lazy authentication initialization was introduced, would a HttpSession be better as this would initialize the authentication on every request?

We use a custom UserDetails class as our principal and the filter checks a mutable flag on it to decide whether to force logging. We can change that at runtime and that worked just fine with 5.7.

Comment From: jzheaux

HttpSession would probably work as well.

You could add the Authentication to HttpServletRequest or HttpSession, yes. I'd recommend HttpServletRequest to make the storage more ephemeral. SecurityContextHolderFilter will reload it into SecurityContextHolder, so the filter should still work the same:

ttp
    .addFilterBefore((request, response, chain) -> {
        request.setAttribute("authentication", SecurityContextHolder.getContext().getAuthentication());
        chain.doFilter(request, response);
    }, AuthorizationFilter.class)
// ...

@Override
public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {
    Authentication authentication = (Authentication) RequestContextHolder.getRequestAttributes()
        .getAttribute("authentication", RequestAttributes.SCOPE_REQUEST);
    return FilterReply.NEUTRAL;
}

would a HttpSession be better as this would initialize the authentication on every request

I'm not sure that I understand this comment. Can you clarify with some code?

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: spring-projects-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.