VYPR
High severity7.5GHSA Advisory· Published Sep 16, 2025· Updated Apr 15, 2026

CVE-2025-41249

CVE-2025-41249

Description

The Spring Framework annotation detection mechanism may not correctly resolve annotations on methods within type hierarchies with a parameterized super type with unbounded generics. This can be an issue if such annotations are used for authorization decisions.

Your application may be affected by this if you are using Spring Security's @EnableMethodSecurity feature.

You are not affected by this if you are not using @EnableMethodSecurity or if you do not use security annotations on methods in generic superclasses or generic interfaces.

This CVE is published in conjunction with CVE-2025-41248 https://spring.io/security/cve-2025-41248 .

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.springframework:spring-coreMaven
>= 5.3.0, <= 5.3.44
org.springframework:spring-coreMaven
>= 6.0.0, <= 6.1.22
org.springframework:spring-coreMaven
>= 6.2.0, < 6.2.116.2.11

Affected products

1

Patches

1
6d710d482a67

Find annotation on overridden method in type hierarchy with unresolved generics

5 files changed · +209 8
  • spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java+1 1 modified
    @@ -204,7 +204,7 @@ private boolean isOverrideFor(Method candidate) {
     		}
     		for (int i = 0; i < paramTypes.length; i++) {
     			if (paramTypes[i] !=
    -					ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) {
    +					ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).toClass()) {
     				return false;
     			}
     		}
    
  • spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java+1 1 modified
    @@ -379,7 +379,7 @@ private static boolean hasSameGenericTypeParameters(
     		}
     		for (int i = 0; i < rootParameterTypes.length; i++) {
     			Class<?> resolvedParameterType = ResolvableType.forMethodParameter(
    -					candidateMethod, i, sourceDeclaringClass).resolve();
    +					candidateMethod, i, sourceDeclaringClass).toClass();
     			if (rootParameterTypes[i] != resolvedParameterType) {
     				return false;
     			}
    
  • spring-core/src/test/java/org/springframework/core/annotation/AnnotatedMethodTests.java+116 0 added
    @@ -0,0 +1,116 @@
    +/*
    + * Copyright 2002-present the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.springframework.core.annotation;
    +
    +import java.lang.annotation.Retention;
    +import java.lang.annotation.RetentionPolicy;
    +import java.lang.reflect.Method;
    +
    +import org.junit.jupiter.api.Test;
    +
    +import org.springframework.core.MethodParameter;
    +import org.springframework.util.ClassUtils;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +/**
    + * Tests for {@link AnnotatedMethod}.
    + *
    + * @author Sam Brannen
    + * @since 6.2.11
    + */
    +class AnnotatedMethodTests {
    +
    +	@Test
    +	void shouldFindAnnotationOnMethodInGenericAbstractSuperclass() {
    +		Method processTwo = getMethod("processTwo", String.class);
    +
    +		AnnotatedMethod annotatedMethod = new AnnotatedMethod(processTwo);
    +
    +		assertThat(annotatedMethod.hasMethodAnnotation(Handler.class)).isTrue();
    +	}
    +
    +	@Test
    +	void shouldFindAnnotationOnMethodInGenericInterface() {
    +		Method processOneAndTwo = getMethod("processOneAndTwo", Long.class, Object.class);
    +
    +		AnnotatedMethod annotatedMethod = new AnnotatedMethod(processOneAndTwo);
    +
    +		assertThat(annotatedMethod.hasMethodAnnotation(Handler.class)).isTrue();
    +	}
    +
    +	@Test
    +	void shouldFindAnnotationOnMethodParameterInGenericAbstractSuperclass() {
    +		Method processTwo = getMethod("processTwo", String.class);
    +
    +		AnnotatedMethod annotatedMethod = new AnnotatedMethod(processTwo);
    +		MethodParameter[] methodParameters = annotatedMethod.getMethodParameters();
    +
    +		assertThat(methodParameters).hasSize(1);
    +		assertThat(methodParameters[0].hasParameterAnnotation(Param.class)).isTrue();
    +	}
    +
    +	@Test
    +	void shouldFindAnnotationOnMethodParameterInGenericInterface() {
    +		Method processOneAndTwo = getMethod("processOneAndTwo", Long.class, Object.class);
    +
    +		AnnotatedMethod annotatedMethod = new AnnotatedMethod(processOneAndTwo);
    +		MethodParameter[] methodParameters = annotatedMethod.getMethodParameters();
    +
    +		assertThat(methodParameters).hasSize(2);
    +		assertThat(methodParameters[0].hasParameterAnnotation(Param.class)).isFalse();
    +		assertThat(methodParameters[1].hasParameterAnnotation(Param.class)).isTrue();
    +	}
    +
    +
    +	private static Method getMethod(String name, Class<?>...parameterTypes) {
    +		return ClassUtils.getMethod(GenericInterfaceImpl.class, name, parameterTypes);
    +	}
    +
    +
    +	@Retention(RetentionPolicy.RUNTIME)
    +	@interface Handler {
    +	}
    +
    +	@Retention(RetentionPolicy.RUNTIME)
    +	@interface Param {
    +	}
    +
    +	interface GenericInterface<A, B> {
    +
    +		@Handler
    +		void processOneAndTwo(A value1, @Param B value2);
    +	}
    +
    +	abstract static class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
    +
    +		@Override
    +		public void processOneAndTwo(Long value1, C value2) {
    +		}
    +
    +		@Handler
    +		public abstract void processTwo(@Param C value);
    +	}
    +
    +	static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
    +
    +		@Override
    +		public void processTwo(String value) {
    +		}
    +	}
    +
    +}
    
  • spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java+29 0 modified
    @@ -945,6 +945,15 @@ void getFromMethodWithGenericSuperclass() throws Exception {
     				Order.class).getDistance()).isEqualTo(0);
     	}
     
    +	@Test
    +	void getFromMethodWithUnresolvedGenericsInGenericTypeHierarchy() {
    +		// The following method is GenericAbstractSuperclass.processOneAndTwo(java.lang.Long, C),
    +		// where 'C' is an unresolved generic, for which ResolvableType.resolve() returns null.
    +		Method method = ClassUtils.getMethod(GenericInterfaceImpl.class, "processOneAndTwo", Long.class, Object.class);
    +		assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY)
    +				.get(Transactional.class).isDirectlyPresent()).isTrue();
    +	}
    +
     	@Test
     	void getFromMethodWithInterfaceOnSuper() throws Exception {
     		Method method = SubOfImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo");
    @@ -3032,6 +3041,26 @@ public void foo(String t) {
     		}
     	}
     
    +	interface GenericInterface<A, B> {
    +
    +		@Transactional
    +		void processOneAndTwo(A value1, B value2);
    +	}
    +
    +	abstract static class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
    +
    +		@Override
    +		public void processOneAndTwo(Long value1, C value2) {
    +		}
    +	}
    +
    +	static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
    +		// The compiler does not require us to declare a concrete
    +		// processOneAndTwo(Long, String) method, and we intentionally
    +		// do not declare one here.
    +	}
    +
    +
     	@Retention(RetentionPolicy.RUNTIME)
     	@Inherited
     	@interface MyRepeatableContainer {
    
  • spring-web/src/test/java/org/springframework/web/method/HandlerMethodTests.java+62 6 modified
    @@ -35,32 +35,46 @@
      * Tests for {@link HandlerMethod}.
      *
      * @author Rossen Stoyanchev
    + * @author Sam Brannen
      */
     class HandlerMethodTests {
     
     	@Test
    -	void shouldValidateArgsWithConstraintsDirectlyOnClass() {
    +	void shouldValidateArgsWithConstraintsDirectlyInClass() {
     		Object target = new MyClass();
     		testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople", "addNames"), true);
     		testValidateArgs(target, List.of("addPerson", "getPerson", "getIntValue", "addPersonNotValidated"), false);
     	}
     
     	@Test
    -	void shouldValidateArgsWithConstraintsOnInterface() {
    +	void shouldValidateArgsWithConstraintsInInterface() {
     		Object target = new MyInterfaceImpl();
     		testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople"), true);
     		testValidateArgs(target, List.of("addPerson", "addPersonNotValidated", "getPerson", "getIntValue"), false);
     	}
     
     	@Test
    -	void shouldValidateReturnValueWithConstraintsDirectlyOnClass() {
    +	void shouldValidateArgsWithConstraintsInGenericAbstractSuperclass() {
    +		Object target = new GenericInterfaceImpl();
    +		shouldValidateArguments(getHandlerMethod(target, "processTwo", String.class), true);
    +	}
    +
    +	@Test
    +	void shouldValidateArgsWithConstraintsInGenericInterface() {
    +		Object target = new GenericInterfaceImpl();
    +		shouldValidateArguments(getHandlerMethod(target, "processOne", Long.class), false);
    +		shouldValidateArguments(getHandlerMethod(target, "processOneAndTwo", Long.class, Object.class), true);
    +	}
    +
    +	@Test
    +	void shouldValidateReturnValueWithConstraintsDirectlyInClass() {
     		Object target = new MyClass();
     		testValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
     		testValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
     	}
     
     	@Test
    -	void shouldValidateReturnValueWithConstraintsOnInterface() {
    +	void shouldValidateReturnValueWithConstraintsInInterface() {
     		Object target = new MyInterfaceImpl();
     		testValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
     		testValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
    @@ -97,9 +111,19 @@ void resolvedFromHandlerMethod() {
     		assertThat(hm3.getResolvedFromHandlerMethod()).isSameAs(hm1);
     	}
     
    +
    +	private static void shouldValidateArguments(HandlerMethod handlerMethod, boolean expected) {
    +		if (expected) {
    +			assertThat(handlerMethod.shouldValidateArguments()).as(handlerMethod.getMethod().getName()).isTrue();
    +		}
    +		else {
    +			assertThat(handlerMethod.shouldValidateArguments()).as(handlerMethod.getMethod().getName()).isFalse();
    +		}
    +	}
    +
     	private static void testValidateArgs(Object target, List<String> methodNames, boolean expected) {
     		for (String methodName : methodNames) {
    -			assertThat(getHandlerMethod(target, methodName).shouldValidateArguments()).isEqualTo(expected);
    +			shouldValidateArguments(getHandlerMethod(target, methodName), expected);
     		}
     	}
     
    @@ -110,7 +134,11 @@ private static void testValidateReturnValue(Object target, List<String> methodNa
     	}
     
     	private static HandlerMethod getHandlerMethod(Object target, String methodName) {
    -		Method method = ClassUtils.getMethod(target.getClass(), methodName, (Class<?>[]) null);
    +		return getHandlerMethod(target, methodName, (Class<?>[]) null);
    +	}
    +
    +	private static HandlerMethod getHandlerMethod(Object target, String methodName, Class<?>... parameterTypes) {
    +		Method method = ClassUtils.getMethod(target.getClass(), methodName, parameterTypes);
     		return new HandlerMethod(target, method).createWithValidateFlags();
     	}
     
    @@ -236,4 +264,32 @@ public Person getPerson() {
     		}
     	}
     
    +
    +	interface GenericInterface<A, B> {
    +
    +		void processOne(@Valid A value1);
    +
    +		void processOneAndTwo(A value1, @Max(42) B value2);
    +	}
    +
    +	abstract static class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
    +
    +		@Override
    +		public void processOne(Long value1) {
    +		}
    +
    +		@Override
    +		public void processOneAndTwo(Long value1, C value2) {
    +		}
    +
    +		public abstract void processTwo(@Max(42) C value);
    +	}
    +
    +	static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
    +
    +		@Override
    +		public void processTwo(String value) {
    +		}
    +	}
    +
     }
    

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

6

News mentions

0

No linked articles in our index yet.