VYPR
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.

PackageAffected versionsPatched versions
org.springframework.security:spring-security-coreMaven
>= 6.4.0, < 6.4.46.4.4

Patches

1
dc2e1af2dab8

Align 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

4

News mentions

0

No linked articles in our index yet.