Securing Spring AI Tool Calls: Stop Letting AI Agents Bypass Authorization with ScopedValue
As enterprise AI agents transition from read-only search to executing critical write-actions via tools, we are seeing a massive surge in agentic privilege escalation vulnerabilities. If you are building agentic workflows in Spring Boot and relying on standard ThreadLocal security contexts, you are likely leaving a back door wide open during asynchronous ReAct execution loops.
Why Most Developers Get This Wrong
- Relying on ThreadLocal in Async ReAct Loops: Spring Security’s default
SecurityContextHolderusesThreadLocal, which silently drops the user context when Spring AI hands off tool execution to virtual threads or asynchronous LLM callback pools. - The "God Mode" Service Account Fallback: Developers lazily run LLM tool executions under a system-level administrative context, allowing a compromised user prompt to execute unauthorized tool calls (e.g., deleting a database record via a tool).
- Lack of Declarative Tool-Level Authorization: Exposing Spring AI
@Toolmethods without binding them to the initiating user's specific OAuth2 scopes or roles, trusting the LLM to self-police.
The Right Way
To prevent agentic privilege escalation, you must propagate the initiating user's security context across the entire agent execution boundary using Java's structured concurrency primitives.
- Leverage ScopedValue for Propagation: Use Java 21+
ScopedValueto securely bind the user'sSecurityContextacross the asynchronous boundaries of the Spring AI agent's ReAct loop. - Enforce Declarative Authorization on Tool Beans: Secure your tool methods directly using Spring Security’s
@PreAuthorizeannotations, ensuring the LLM cannot invoke a tool the initiating user doesn't have rights to. - Implement Immutable Contexts: Ensure the propagated security context is strictly read-only within the execution scope to prevent the LLM from poisoning or escalating its own privileges.
I built javalld.com while prepping for senior roles — complete LLD problems with execution traces, not just theory.
Show Me The Code
@Component
public class SecureAgentService {
public static final ScopedValue<SecurityContext> SEC_CONTEXT = ScopedValue.newInstance();
@Tool(description = "Transfer funds")
@PreAuthorize("hasRole('USER')") // Evaluated against SEC_CONTEXT in the virtual thread
public String transfer(double amount) {
return "Transferred $" + amount;
}
public String run(String prompt) {
return ScopedValue.where(SEC_CONTEXT, SecurityContextHolder.getContext())
.call(() -> chatClient.prompt(prompt).call().content());
}
}
Key Takeaways
- ThreadLocals are Dead for Agents: Async LLM reasoning loops break standard Spring Security thread-binding; migrate to
ScopedValuefor robust concurrent propagation. - Zero-Trust Tooling: Never trust the model to self-police; apply strict
@PreAuthorizerules on every single@Toolbean. - Fail-Closed Architecture: If the security context cannot be resolved within the tool execution thread, fail closed immediately with an access denial.
Top comments (0)