Skip to content

Commit 8edc7cd

Browse files
YongGoosesbrannen
andcommitted
Reject effectively private handler methods on CGLIB proxied controllers
This commit rejects the invocation of an effectively private handler method on a CGLIB proxied controller for both Spring MVC and Spring WebFlux. Closes gh-35352 Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> Signed-off-by: Yongjun Hong <kevin0928@naver.com> Signed-off-by: yongjunhong <yongjunh@apache.org>
1 parent a5d9bd6 commit 8edc7cd

File tree

5 files changed

+304
-1
lines changed

5 files changed

+304
-1
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.testfixture.method;
18+
19+
import org.springframework.stereotype.Controller;
20+
import org.springframework.web.bind.annotation.RequestMapping;
21+
22+
public class VisibilityTestHandler {
23+
24+
@Controller
25+
public static class PackagePrivateController {
26+
@RequestMapping("/package-private")
27+
void packagePrivateMethod() {
28+
}
29+
}
30+
31+
@Controller
32+
public static class ProtectedController {
33+
@RequestMapping("/protected")
34+
protected void protectedMethod() {
35+
}
36+
}
37+
}

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
2121
import java.lang.reflect.Method;
22+
import java.lang.reflect.Modifier;
2223
import java.util.Collections;
2324
import java.util.LinkedHashMap;
2425
import java.util.List;
@@ -40,6 +41,7 @@
4041
import org.springframework.core.annotation.RepeatableContainers;
4142
import org.springframework.stereotype.Controller;
4243
import org.springframework.util.Assert;
44+
import org.springframework.util.ClassUtils;
4345
import org.springframework.util.CollectionUtils;
4446
import org.springframework.util.StringUtils;
4547
import org.springframework.util.StringValueResolver;
@@ -67,6 +69,7 @@
6769
* @author Rossen Stoyanchev
6870
* @author Sam Brannen
6971
* @author Olga Maciaszek-Sharma
72+
* @author Yongjun Hong
7073
* @since 5.0
7174
*/
7275
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
@@ -158,13 +161,21 @@ protected boolean isHandler(Class<?> beanType) {
158161
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
159162
* and {@link HttpExchange @HttpExchange} annotations to create the
160163
* {@link RequestMappingInfo}.
164+
* <p>For CGLIB proxy classes, additional validation is performed based on method visibility:
165+
* <ul>
166+
* <li>Private methods cannot be overridden and therefore cannot be used as handler methods.</li>
167+
* <li>Package-private methods from different packages are inaccessible and must be
168+
* changed to public or protected.</li>
169+
* </ul>
161170
* @return the created {@code RequestMappingInfo}, or {@code null} if the method
162171
* does not have a {@code @RequestMapping} or {@code @HttpExchange} annotation
163172
* @see #getCustomMethodCondition(Method)
164173
* @see #getCustomTypeCondition(Class)
165174
*/
166175
@Override
167176
protected @Nullable RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
177+
validateCglibProxyMethodVisibility(method, handlerType);
178+
168179
RequestMappingInfo info = createRequestMappingInfo(method);
169180
if (info != null) {
170181
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
@@ -189,6 +200,37 @@ protected boolean isHandler(Class<?> beanType) {
189200
return info;
190201
}
191202

203+
private void validateCglibProxyMethodVisibility(Method method, Class<?> handlerType) {
204+
if (isCglibProxy(handlerType)) {
205+
int modifiers = method.getModifiers();
206+
207+
if (Modifier.isPrivate(modifiers)) {
208+
throw new IllegalStateException(
209+
"Private method [" + method.getName() + "] on CGLIB proxy class [" + handlerType.getName() +
210+
"] cannot be used as a request handler method because private methods cannot be overridden. " +
211+
"Change the method to non-private visibility or use interface-based JDK proxying instead.");
212+
}
213+
214+
if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers)) {
215+
Class<?> declaringClass = method.getDeclaringClass();
216+
Package methodPackage = declaringClass.getPackage();
217+
Package handlerPackage = handlerType.getPackage();
218+
219+
if (!Objects.equals(methodPackage, handlerPackage)) {
220+
throw new IllegalStateException(
221+
"Package-private method [" + method.getName() + "] on CGLIB proxy class [" + declaringClass.getName() +
222+
"] from package [" + methodPackage.getName() + "] cannot be advised when used by handler class [" +
223+
handlerType.getName() + "] from package [" + handlerPackage.getName() + "] because it is effectively private. " +
224+
"Either make the method public/protected or use interface-based JDK proxying instead.");
225+
}
226+
}
227+
}
228+
}
229+
230+
private boolean isCglibProxy(Class<?> beanType) {
231+
return beanType.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR);
232+
}
233+
192234
private @Nullable RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
193235

194236
List<AnnotationDescriptor> descriptors =

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.junit.jupiter.api.BeforeEach;
2929
import org.junit.jupiter.api.Test;
3030

31+
import org.springframework.cglib.proxy.Enhancer;
32+
import org.springframework.cglib.proxy.NoOp;
3133
import org.springframework.core.annotation.AliasFor;
3234
import org.springframework.http.MediaType;
3335
import org.springframework.stereotype.Controller;
@@ -60,13 +62,16 @@
6062
import static org.mockito.Mockito.mock;
6163
import static org.springframework.web.bind.annotation.RequestMethod.POST;
6264
import static org.springframework.web.reactive.result.method.RequestMappingInfo.paths;
65+
import static org.springframework.web.testfixture.method.VisibilityTestHandler.PackagePrivateController;
66+
import static org.springframework.web.testfixture.method.VisibilityTestHandler.ProtectedController;
6367

6468
/**
6569
* Tests for {@link RequestMappingHandlerMapping}.
6670
*
6771
* @author Rossen Stoyanchev
6872
* @author Olga Maciaszek-Sharma
6973
* @author Sam Brannen
74+
* @author Yongjun Hong
7075
*/
7176
class RequestMappingHandlerMappingTests {
7277

@@ -409,6 +414,93 @@ void requestBodyAnnotationFromImplementationOverridesInterface() {
409414
assertThat(matchingInfo).isEqualTo(paths(path).methods(POST).consumes(mediaType.toString()).build());
410415
}
411416

417+
@Test
418+
void privateMethodOnCglibProxyShouldThrowException() throws Exception {
419+
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
420+
421+
Class<?> handlerType = PrivateMethodController.class;
422+
Method method = handlerType.getDeclaredMethod("privateMethod");
423+
424+
Class<?> proxyClass = createProxyClass(handlerType);
425+
426+
assertThatIllegalStateException()
427+
.isThrownBy(() -> mapping.getMappingForMethod(method, proxyClass))
428+
.withMessageContainingAll(
429+
"Private method [privateMethod]",
430+
"cannot be used as a request handler method"
431+
);
432+
}
433+
434+
@Test
435+
void protectedMethodShouldNotThrowException() throws Exception {
436+
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
437+
438+
Class<?> handlerType = ProtectedMethodController.class;
439+
Method method = handlerType.getDeclaredMethod("protectedMethod");
440+
Class<?> proxyClass = createProxyClass(handlerType);
441+
442+
RequestMappingInfo info = mapping.getMappingForMethod(method, proxyClass);
443+
444+
assertThat(info.getPatternsCondition().getDirectPaths()).containsOnly("/protected");
445+
}
446+
447+
@Test
448+
void differentPackagePackagePrivateMethodShouldThrowException() throws Exception {
449+
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
450+
451+
Class<?> handlerType = ControllerWithPackagePrivateClass.class;
452+
Method method = PackagePrivateController.class.getDeclaredMethod("packagePrivateMethod");
453+
454+
Class<?> proxyClass = createProxyClass(handlerType);
455+
456+
assertThatIllegalStateException()
457+
.isThrownBy(() -> mapping.getMappingForMethod(method, proxyClass))
458+
.withMessageContainingAll(
459+
"Package-private method [packagePrivateMethod]",
460+
"cannot be advised when used by handler class"
461+
);
462+
}
463+
464+
@Test
465+
void differentPackageProtectedMethodShouldNotThrowException() throws Exception {
466+
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
467+
468+
Class<?> handlerType = ControllerWithProtectedClass.class;
469+
Method method = ProtectedController.class.getDeclaredMethod("protectedMethod");
470+
471+
Class<?> proxyClass = createProxyClass(handlerType);
472+
473+
RequestMappingInfo info = mapping.getMappingForMethod(method, proxyClass);
474+
assertThat(info.getPatternsCondition().getDirectPaths()).containsOnly("/protected");
475+
}
476+
477+
478+
private Class<?> createProxyClass(Class<?> targetClass) {
479+
Enhancer enhancer = new Enhancer();
480+
enhancer.setSuperclass(targetClass);
481+
enhancer.setCallbackTypes(new Class[]{NoOp.class});
482+
483+
return enhancer.createClass();
484+
}
485+
486+
@Controller
487+
static class PrivateMethodController {
488+
@RequestMapping("/private")
489+
private void privateMethod() {}
490+
}
491+
492+
@Controller
493+
static class ProtectedMethodController {
494+
@RequestMapping("/protected")
495+
protected void protectedMethod() {}
496+
}
497+
498+
@Controller
499+
static class ControllerWithPackagePrivateClass extends PackagePrivateController { }
500+
501+
@Controller
502+
static class ControllerWithProtectedClass extends ProtectedController { }
503+
412504
private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) {
413505
String methodName = requestMethod.name().toLowerCase();
414506
String path = "/" + methodName;

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
2121
import java.lang.reflect.Method;
22+
import java.lang.reflect.Modifier;
2223
import java.util.Collections;
2324
import java.util.LinkedHashMap;
2425
import java.util.List;
@@ -41,6 +42,7 @@
4142
import org.springframework.core.annotation.RepeatableContainers;
4243
import org.springframework.stereotype.Controller;
4344
import org.springframework.util.Assert;
45+
import org.springframework.util.ClassUtils;
4446
import org.springframework.util.CollectionUtils;
4547
import org.springframework.util.StringUtils;
4648
import org.springframework.util.StringValueResolver;
@@ -72,6 +74,7 @@
7274
* @author Rossen Stoyanchev
7375
* @author Sam Brannen
7476
* @author Olga Maciaszek-Sharma
77+
* @author Yongjun Hong
7578
* @since 3.1
7679
*/
7780
@SuppressWarnings("removal")
@@ -184,13 +187,21 @@ protected boolean isHandler(Class<?> beanType) {
184187
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
185188
* and {@link HttpExchange @HttpExchange} annotations to create the
186189
* {@link RequestMappingInfo}.
190+
* <p>For CGLIB proxy classes, additional validation is performed based on method visibility:
191+
* <ul>
192+
* <li>Private methods cannot be overridden and therefore cannot be used as handler methods.</li>
193+
* <li>Package-private methods from different packages are inaccessible and must be
194+
* changed to public or protected.</li>
195+
* </ul>
187196
* @return the created {@code RequestMappingInfo}, or {@code null} if the method
188197
* does not have a {@code @RequestMapping} or {@code @HttpExchange} annotation
189198
* @see #getCustomMethodCondition(Method)
190199
* @see #getCustomTypeCondition(Class)
191200
*/
192201
@Override
193202
protected @Nullable RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
203+
validateCglibProxyMethodVisibility(method, handlerType);
204+
194205
RequestMappingInfo info = createRequestMappingInfo(method);
195206
if (info != null) {
196207
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
@@ -208,6 +219,38 @@ protected boolean isHandler(Class<?> beanType) {
208219
return info;
209220
}
210221

222+
private void validateCglibProxyMethodVisibility(Method method, Class<?> handlerType) {
223+
if (isCglibProxy(handlerType)) {
224+
int modifiers = method.getModifiers();
225+
226+
if (Modifier.isPrivate(modifiers)) {
227+
throw new IllegalStateException(
228+
"Private method [" + method.getName() + "] on CGLIB proxy class [" + handlerType.getName() +
229+
"] cannot be used as a request handler method because private methods cannot be overridden. " +
230+
"Change the method to non-private visibility or use interface-based JDK proxying instead.");
231+
}
232+
233+
if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers)) {
234+
Class<?> declaringClass = method.getDeclaringClass();
235+
Package methodPackage = declaringClass.getPackage();
236+
Package handlerPackage = handlerType.getPackage();
237+
238+
if (!Objects.equals(methodPackage, handlerPackage)) {
239+
throw new IllegalStateException(
240+
"Package-private method [" + method.getName() + "] on CGLIB proxy class [" + declaringClass.getName() +
241+
"] from package [" + methodPackage.getName() + "] cannot be advised when used by handler class [" +
242+
handlerType.getName() + "] from package [" + handlerPackage.getName() + "] because it is effectively private. " +
243+
"Either make the method public/protected or use interface-based JDK proxying instead.");
244+
}
245+
}
246+
}
247+
}
248+
249+
private boolean isCglibProxy(Class<?> beanType) {
250+
return beanType.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR);
251+
}
252+
253+
211254
@Nullable String getPathPrefix(Class<?> handlerType) {
212255
for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
213256
if (entry.getValue().test(handlerType)) {

0 commit comments

Comments
 (0)