This article describes a security lab demonstrating how AI agents that persist state in client-side storage can be exploited by tampering with privilege fields. StateBot stores its role as a JSON blob in browser storage and trusts the role field on reload without verification, allowing an attacker to change 'user' to 'admin' and unlock protected functionality. The lab illustrates a classic broken-access-control vulnerability reappearing in modern agent architectures.
- StateBot's stateless trust mechanismStateBot persists {role: 'user'} in client storage, reloads it on every request, and branches its behavior based on the role value. The role field determines whether the agent takes a normal path or unlocks an admin-only get_secret_config path. Critically, the blob is neither signed nor re-verified after reload, so an edited role field is immediately trusted.
- Attack requires three simple stepsAn attacker locates the persisted JSON state in client storage, edits the role field from 'user' to 'admin', and then triggers a request so the tampered state reloads. When the agent branches on the now-admin role, it executes the get_secret_config path and returns the secret configuration, completing the exploit.
- Client-held state is inherently attacker-controlledThe core vulnerability stems from storing authoritative privilege information where the user can edit it. Any field that controls behavior or access decisions must either remain on the server or be cryptographically signed and verified on every load. Selective signing of only certain fields leaves unprotected fields like role editable.
- Connection to Excessive Agency in LLMsThe article identifies this as enabling Excessive Agency (LLM06), a vulnerability where users flip an agent's privileges by tampering with reloaded state. As LLM agents increasingly use portable, client-side memory designs, this classic broken-access-control pattern re-emerges in new contexts.
- Defense requires server-side authorityThe recommended defense keeps authoritative state on the server, keyed to an authenticated session, ensuring the client cannot unilaterally change its own privileges. If state must travel with the client, the entire blob must be signed and verified on load—not just selected fields—and privileges must be derived from the server-side session, never from reloaded state.
- Historical pattern in modern architectureThe article notes this is a long-standing broken-access-control pattern that echoes classic insecure-deserialization and mass-assignment bugs, now reappearing as portable agent memory and state designs become more common. Developers building agent architectures must recognize that storing state on the client reintroduces this well-known vulnerability.
Bottom line: Any AI agent or application that persists state to the client and acts on privilege fields from that state is trusting the user to enforce their own restrictions. Authoritative privilege information must remain server-side and verified on every use, or the entire state blob must be cryptographically signed and validated before the agent acts on it.
role field to decide what it can do. State you can edit is state you control — so set the role to admin and the admin code path opens.📋 In this walkthrough
What this lab teaches
This is the agentic version of trusting client-side state. The agent persists its state where the user can reach it, then reloads and acts on fields like role without verifying them. Whoever can edit the stored blob decides the agent's privileges.
The lesson: client-held state is attacker-controlled. A field that changes behaviour must be authenticated or kept on the server.
How StateBot reloads its state
StateBot stores {…, role: 'user'} in client storage. Each request reloads the JSON and branches on role: 'user' takes the normal path, 'admin' unlocks a get_secret_config path. Nothing signs or re-checks the blob, so an edited role is trusted.
The attack approach
- Find the agent state persisted in client storage.
- Locate the role field inside the JSON blob.
- Change it from user to admin and let the state reload.
- Trigger the admin-only code path to read the secret config.
Full walkthrough (spoiler)
If the change does not stick, you may be editing the wrong copy — open the steps.
Show the step-by-step solution
- Open the persisted JSON state StateBot keeps in client storage.
- Set the role field to admin and save it back.
- Make a request so the server reloads the tampered state and branches to the admin path.
- The admin-only get_secret_config path runs and returns SECRET_CONFIG. The lab marks you solved.
Why it works
The server treats the reloaded blob as authoritative, but it lives on the client where you can edit it. Because only selected fields (if any) are protected and the role is not, changing role rewrites what the agent is allowed to do.
Where this happens in the real world
Trusting tampered client state is a long-standing broken-access-control pattern, now reappearing in agent memory and 'portable state' designs. In LLM terms it enables Excessive Agency (LLM06) by letting the user flip the agent's privileges, and it echoes classic insecure-deserialization and mass-assignment bugs.
How to defend against it
- Keep authoritative state on the server, keyed to an authenticated session.
- If state must travel with the client, sign the whole blob and verify it on every load — not just selected fields.
- Derive privileges from the server-side session, never from a field in reloaded state.
- Validate reloaded state against expected types and values before acting on it.
Try it, then go further
Reading about it is the easy part. Open the lab, try your own approach before peeking at the spoiler, and see how many attempts it takes.
