Medium severity5.3NVD Advisory· Published Mar 24, 2025· Updated Apr 15, 2026
CVE-2025-22223
CVE-2025-22223
Description
Spring Security 6.4.0 - 6.4.3 may not correctly locate method security annotations on parameterized types or methods. This may cause an authorization bypass.
You are not affected if you are not using @EnableMethodSecurity, or you do not have method security annotations on parameterized types or methods, or all method security annotations are attached to target methods
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.springframework.security:spring-security-coreMaven | >= 6.4.0, < 6.4.4 | 6.4.4 |
Patches
1dc2e1af2dab8Align Method Traversal with MergedAnnotations
2 files changed · +115 −7
core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java+55 −7 modified@@ -19,8 +19,10 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -29,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.springframework.core.MethodClassKey; +import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -169,18 +172,15 @@ private List<MergedAnnotation<A>> findClosestMethodAnnotations(Method method, Cl return Collections.emptyList(); } classesToSkip.add(targetClass); - try { - Method methodToUse = targetClass.getDeclaredMethod(method.getName(), method.getParameterTypes()); + Method methodToUse = findMethod(method, targetClass); + if (methodToUse != null) { List<MergedAnnotation<A>> annotations = findDirectAnnotations(methodToUse); if (!annotations.isEmpty()) { return annotations; } } - catch (NoSuchMethodException ex) { - // move on - } - List<MergedAnnotation<A>> annotations = new ArrayList<>(); - annotations.addAll(findClosestMethodAnnotations(method, targetClass.getSuperclass(), classesToSkip)); + List<MergedAnnotation<A>> annotations = new ArrayList<>( + findClosestMethodAnnotations(method, targetClass.getSuperclass(), classesToSkip)); for (Class<?> inter : targetClass.getInterfaces()) { annotations.addAll(findClosestMethodAnnotations(method, inter, classesToSkip)); } @@ -212,4 +212,52 @@ private List<MergedAnnotation<A>> findDirectAnnotations(AnnotatedElement element .toList(); } + private static Method findMethod(Method method, Class<?> targetClass) { + for (Method candidate : targetClass.getDeclaredMethods()) { + if (candidate == method) { + return candidate; + } + if (isOverride(method, candidate)) { + return candidate; + } + } + return null; + } + + private static boolean isOverride(Method rootMethod, Method candidateMethod) { + return (!Modifier.isPrivate(candidateMethod.getModifiers()) + && candidateMethod.getName().equals(rootMethod.getName()) + && hasSameParameterTypes(rootMethod, candidateMethod)); + } + + private static boolean hasSameParameterTypes(Method rootMethod, Method candidateMethod) { + if (candidateMethod.getParameterCount() != rootMethod.getParameterCount()) { + return false; + } + Class<?>[] rootParameterTypes = rootMethod.getParameterTypes(); + Class<?>[] candidateParameterTypes = candidateMethod.getParameterTypes(); + if (Arrays.equals(candidateParameterTypes, rootParameterTypes)) { + return true; + } + return hasSameGenericTypeParameters(rootMethod, candidateMethod, rootParameterTypes); + } + + private static boolean hasSameGenericTypeParameters(Method rootMethod, Method candidateMethod, + Class<?>[] rootParameterTypes) { + + Class<?> sourceDeclaringClass = rootMethod.getDeclaringClass(); + Class<?> candidateDeclaringClass = candidateMethod.getDeclaringClass(); + if (!candidateDeclaringClass.isAssignableFrom(sourceDeclaringClass)) { + return false; + } + for (int i = 0; i < rootParameterTypes.length; i++) { + Class<?> resolvedParameterType = ResolvableType.forMethodParameter(candidateMethod, i, sourceDeclaringClass) + .resolve(); + if (rootParameterTypes[i] != resolvedParameterType) { + return false; + } + } + return true; + } + }
core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java+60 −0 modified@@ -251,6 +251,30 @@ void scanWhenClassInheritingAbstractClassNoAnnotationsThenNoAnnotation() throws assertThat(preAuthorize).isNull(); } + // gh-16751 + @Test + void scanWhenAnnotationOnParameterizedInterfaceTheLocates() throws Exception { + Method method = MyServiceImpl.class.getDeclaredMethod("get", String.class); + PreAuthorize pre = this.scanner.scan(method, method.getDeclaringClass()); + assertThat(pre).isNotNull(); + } + + // gh-16751 + @Test + void scanWhenAnnotationOnParameterizedSuperClassThenLocates() throws Exception { + Method method = MyServiceImpl.class.getDeclaredMethod("getExt", Long.class); + PreAuthorize pre = this.scanner.scan(method, method.getDeclaringClass()); + assertThat(pre).isNotNull(); + } + + // gh-16751 + @Test + void scanWhenAnnotationOnParameterizedMethodThenLocates() throws Exception { + Method method = MyServiceImpl.class.getDeclaredMethod("getExtByClass", Class.class, Long.class); + PreAuthorize pre = this.scanner.scan(method, method.getDeclaringClass()); + assertThat(pre).isNotNull(); + } + @PreAuthorize("one") private interface AnnotationOnInterface { @@ -577,4 +601,40 @@ private static class ClassInheritingAbstractClassNoAnnotations extends AbstractC } + interface MyService<C, U> { + + @PreAuthorize("thirty") + C get(U u); + + } + + abstract static class MyServiceExt<T> implements MyService<Integer, String> { + + @PreAuthorize("thirtyone") + abstract T getExt(T t); + + @PreAuthorize("thirtytwo") + abstract <S extends Number> S getExtByClass(Class<S> clazz, T t); + + } + + static class MyServiceImpl extends MyServiceExt<Long> { + + @Override + public Integer get(final String s) { + return 0; + } + + @Override + Long getExt(Long o) { + return 0L; + } + + @Override + <S extends Number> S getExtByClass(Class<S> clazz, Long l) { + return null; + } + + } + }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.