From 4c829a5d0f7f29326ec8acc3be9b5048c6a20ae6 Mon Sep 17 00:00:00 2001 From: seonwoo_jung Date: Fri, 12 Jun 2026 06:14:11 +0900 Subject: [PATCH] Skip @PostFilter when method returns null PostFilterAuthorizationMethodInterceptor#invoke called MethodSecurityExpressionHandler#filter unconditionally with the value returned by the intercepted method. DefaultMethodSecurityExpressionHandler#filter requires the filter target to be a Collection, array, Map, or Stream and throws IllegalArgumentException for any other value, including null. As a result, any @PostFilter-annotated method that returns null under @EnableMethodSecurity threw IllegalArgumentException instead of returning null to the caller. The legacy ExpressionBasedPostInvocationAdvice, used by the deprecated @EnableGlobalMethodSecurity, explicitly skipped filtering when the returned object was null and logged "Return object is null, filtering will be skipped". The new interceptor introduced as part of the AuthorizationManager-based method security pipeline did not carry that guard over, which made the migration from @EnableGlobalMethodSecurity to @EnableMethodSecurity a behavioural breaking change for callers relying on null as a no-op signal (for example, a service that returns null when there is nothing to filter). Skip the filter call when the returned object is null and log the same debug message as the legacy advice so that @PostFilter on null returns preserves the documented behaviour across both APIs. Closes gh-19280 Signed-off-by: seonwoo_jung --- .../PostFilterAuthorizationMethodInterceptor.java | 8 ++++++++ ...tFilterAuthorizationMethodInterceptorTests.java | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java index 0264e6ecebe..d05a2a26ffc 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java @@ -21,6 +21,8 @@ import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; @@ -44,6 +46,8 @@ */ public final class PostFilterAuthorizationMethodInterceptor implements AuthorizationAdvisor { + private final Log logger = LogFactory.getLog(getClass()); + private Supplier securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy; private PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry(); @@ -133,6 +137,10 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strat if (attribute == null) { return returnedObject; } + if (returnedObject == null) { + this.logger.debug("Returned object is null, filtering will be skipped"); + return null; + } MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); EvaluationContext ctx = expressionHandler.createEvaluationContext(this::getAuthentication, mi); return expressionHandler.filter(returnedObject, attribute.getExpression(), ctx); diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java index 31fee409f95..12710673fc7 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java @@ -109,6 +109,20 @@ public Object proceed() { assertThat(result).asInstanceOf(InstanceOfAssertFactories.array(String[].class)).containsOnly("john"); } + // gh-19280 + @Test + public void invokeWhenReturnedObjectIsNullThenSkipsFilteringAndReturnsNull() throws Throwable { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingArray", new Class[] { String[].class }, new Object[] { null }) { + @Override + public Object proceed() { + return null; + } + }; + PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor(); + assertThat(advice.invoke(methodInvocation)).isNull(); + } + @Test public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception { MockMethodInvocation methodInvocation = new MockMethodInvocation(new ConflictingAnnotations(),