DEV Community

Machine coding Master
Machine coding Master

Posted on

Securing Spring AI Tool Calls: Stop Letting AI Agents Bypass Authorization with ScopedValue

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 SecurityContextHolder uses ThreadLocal, 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 @Tool methods 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+ ScopedValue to securely bind the user's SecurityContext across 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 @PreAuthorize annotations, 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());
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • ThreadLocals are Dead for Agents: Async LLM reasoning loops break standard Spring Security thread-binding; migrate to ScopedValue for robust concurrent propagation.
  • Zero-Trust Tooling: Never trust the model to self-police; apply strict @PreAuthorize rules on every single @Tool bean.
  • 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)