[← 02 · SpEL reference](02-spel-reference.md) · [chapter index](README.md) · [next: non-proxyable methods →](04-non-proxyable-methods.md) # 03 · Self-invocation Run: `java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo2SelfInvocation` Output: [`output/demo2.txt`](output/demo2.txt) Source: [`Demo2SelfInvocation.java`](../src/main/java/com/ankurm/methodsec/Demo2SelfInvocation.java) ## The shape ```java @PreAuthorize("hasRole('ADMIN')") public String adminReport() { return "TOP SECRET REVENUE NUMBERS"; } public String userEntryPoint() { return adminReport(); // no proxy involved } ``` A caller invoking `adminReport()` from outside goes through the proxy and is denied. A caller invoking `userEntryPoint()` reaches the target object, and `this.adminReport()` is a plain virtual call on the target. The advisor never sees it. From [`output/demo2.txt`](output/demo2.txt), as a user holding only `ROLE_USER`: ``` reports.adminReport() (via proxy) DENIED -> AuthorizationDeniedException reports.userEntryPoint() (this.adminReport()) ALLOWED -> TOP SECRET REVENUE NUMBERS ``` ## Why it survives review The same run prints: ``` ReportService.adminReport() @PreAuthorize -> @PreAuthorize("hasRole('ADMIN')") bean is an AOP proxy -> true proxy class -> ...ReportService$$SpringCGLIB$$0 ``` The annotation is present. The bean *is* proxied. Reflection agrees with the source. Every individual check a reviewer would run passes; only the composition is wrong. Nothing logs at any level, no metric moves, and the method returns the right answer to the wrong person. ## Three fixes, in order of preference **Move the method to a different bean.** A call between two beans is an external call by definition. This is the boring answer and it is usually the right one, because a method that needs its own authorization rule is usually a different responsibility. **Inject the bean into itself through an `ObjectProvider`.** ```java private final ObjectProvider self; public String viaSelfInjection() { return this.self.getObject().adminReport(); } ``` A direct field of the bean's own type is a circular reference the container refuses in a constructor; `ObjectProvider` resolves lazily at call time and hands back the proxy. **`AopContext.currentProxy()`.** ```java public String viaAopContext() { return ((ReportService) AopContext.currentProxy()).adminReport(); } ``` This needs `@EnableAspectJAutoProxy(exposeProxy = true)` — without it the call throws `IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available`. It also couples the method to the fact that it is proxied, which is why it is last on this list. All three are verified in the same run; all three produce `AuthorizationDeniedException`. ## The structural fix `@EnableMethodSecurity(mode = AdviceMode.ASPECTJ)` weaves the advice into the bytecode instead of wrapping the object, and self-invocation stops being a thing. It also requires AspectJ weaving in the build, which is a large change to make for one class of bug. Consider it if you are already weaving. The cheaper structural answer is not to depend on method security alone: keep a catch-all `authorizeHttpRequests` rule (`anyRequest().authenticated()`) so that an inner method that escapes its own check is still behind a request-level one. [← 02 · SpEL reference](02-spel-reference.md) · [chapter index](README.md) · [next: non-proxyable methods →](04-non-proxyable-methods.md)