1
0
Files
spring-security-demo/method-security/docs/03-self-invocation.md
asmhatre 5e9e7f1b12 Split into per-article modules and add the method-security module
Moves the existing virtual-thread/context-propagation project into
context-propagation/ and adds method-security/ for the Spring Security 7
method-security article: nine runnable demos, fourteen assertions, and every
transcript the article quotes, regenerated by scripts/run-all.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSrsDSRKVsY588yFiMJMo9
2026-08-25 02:01:29 +00:00

3.5 KiB

← 02 · SpEL reference · chapter index · next: non-proxyable methods →

03 · Self-invocation

Run: java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo2SelfInvocation Output: output/demo2.txt Source: Demo2SelfInvocation.java

The shape

@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, 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.

private final ObjectProvider<ReportService> 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().

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 · chapter index · next: non-proxyable methods →