Meta AI Agent Account Takeover: The Risk of Missing Authorization in Agentic Workflows
Introduction
AI agents are increasingly being connected to sensitive account-management workflows such as password resets, email changes, recovery flows, and support automation. That creates a new problem. The[…]
If an AI agent can call privileged tools without enforcing account ownership, step-up verification, and policy checks, a normal support interaction can become an account takeover path.
This blog uses a simplified support agent to explain the risk. The vulnerability is not the LLM itself, but the missing authorization boundary between the user, the agent, and the privileged tool. The[…]
How the vulnerable agent looks
Below is a simplified example of how this vulnerable pattern can appear.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def ai_support_chat(message: str) -> str:
"""Meta AI support: understands intent, executes immediately."""
intent = classify_intent(message)
if intent == "change_email":
username = extract_username(message)
new_email = extract_email(message)
accounts_db[username]["email"] = new_email
return f"Done! Email for @{username} updated to {new_email}."
if intent == "reset_password":
username = extract_username(message)
new_password = generate_temp_password()
send_email(accounts_db[username]["email"], f"New password: {new_password}")
return f"Password reset! Check {accounts_db[username]['email']}."
return "I can help with account recovery. What do you need?"
This is simplified pseudocode, but the pattern is real. The agent understands the request, extracts parameters, and calls a privileged function. The dangerous part is that the tool performs the action[…]
The issue is not that the agent can understand:
Change the email for @target_user to attacker@example.com
The backend tool accepts that request and performs the mutation without checking ownership.
Vulnerable flow
sequenceDiagram
participant U as User
participant AI as Meta AI Chat
participant T as Tool: change_email
U->>AI: Link my email to @obama_whitehouse
AI->>T: change_email(username, new_email)
Note over T: No ownership check
T-->>AI: Email updated to hacker@evil.com
AI-->>U: Done! Email changed.
The account takeover chain is simple:
- The attacker asks the AI agent to change the email address of a target account.
- The agent extracts the target username from the message.
- The agent extracts the attacker-controlled email address.
- The agent calls the
change_emailtool. - The tool updates the email without checking ownership.
- The attacker triggers password reset.
- The password reset goes to the attacker-controlled email.
- The attacker takes over the account.
This is a classic broken access control issue, but agentic workflows make it easier to trigger through natural language.
Designs I have seen while testing agentic workflows
While assessing agents and testing agentic workflows, I have seen different designs used to control privileged actions. Some designs place the authorization logic inside the tool itself. Others use a […]
There is no single perfect design that works for every product. The important point is that the agent should never have a direct path from understanding a user request to executing a privileged action[…]
Below are three designs I have seen or used when evaluating how agentic systems handle sensitive actions.
First design: The agent does not perform the mutation
In this approach, when the user requests an email change, the agent does not directly change the email address. It only starts the verified account workflow.
1
2
3
4
5
6
7
8
9
10
def ai_support_chat(message: str) -> str:
intent = classify_intent(message)
if intent == "change_email":
username = extract_username(message)
email = accounts_db[username]["email"]
requests.post("https://api.instagram.com/auth/forgot-password",
json={"email": email})
return f"Verification sent to {mask_email(email)}. Check your email to complete the change."
The key idea here is that the agent never handles secrets and never performs the final mutation. It can initiate the process, but the user must complete the flow through the already verified channel.
sequenceDiagram
participant U as User
participant AI as AI Agent
participant T as Tool: change_email
U->>AI: Change my email
AI->>T: change_email(username, new_email, auth_token=None)
T-->>U: Token sent to verified email
U->>T: change_email(username, new_email, auth_token=REAL_TOKEN)
T-->>U: Email updated
Note over AI: Agent never sees auth_token
This reduces the agent’s authority. The agent is no longer able to directly mutate the account. It only routes the user to a verified recovery or confirmation flow.
How this design can still be abused
This design prevents the agent from directly changing the account email, but the verification flow itself can still become an abuse surface.
If the agent can initiate recovery or verification flows for any username, an attacker may repeatedly trigger verification emails for a victim account. This does not directly give the attacker access,[…]
It can also create a phishing opportunity. For example, an attacker may flood the victim with legitimate verification emails, then follow up with a fake message pretending to be support.
A safer implementation should not allow the agent to trigger verification flows freely. The system should verify that the requester is allowed to initiate the flow, apply rate limits, collapse duplica[…]
Second design: Privileged tools require verification
In this approach, the agent can call the tool, but the tool itself enforces a separate authorization step. The agent cannot bypass that step.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def change_email(username: str, new_email: str, auth_token: str = None) -> str:
# Phase 1: No token → just send verification
if not auth_token:
token = secrets.token_urlsafe(32)
store_verification(username, token, action="change_email", payload=new_email)
send_email(accounts_db[username]["email"], f"Confirm: /verify?token={token}")
return f"Verification sent. Provide token to complete."
# Phase 2: Token provided → execute
pending = get_pending_action(username, auth_token)
if not pending or pending["action"] != "change_email":
return "Invalid or expired token."
accounts_db[username]["email"] = new_email
return "Email updated."
def ai_support_chat(message: str) -> str:
intent = classify_intent(message)
if intent == "change_email":
username, new_email = extract_details(message)
return change_email(username, new_email) # No token → only Phase 1
The important point is that calling the tool without a valid token does not update the email. It only sends a verification request. The mutation happens only after the valid token is provided.
sequenceDiagram
participant U as User
participant AI as AI Agent
participant T as Tool: change_email
U->>AI: Change my email
AI->>T: change_email(username, new_email, auth_token=None)
T-->>U: Token sent to verified email
U->>T: change_email(username, new_email, auth_token=REAL_TOKEN)
T-->>U: Email updated
Note over AI: Agent never sees auth_token
This design is better than allowing the agent to execute privileged actions directly. The tool becomes responsible for enforcing the security boundary. Even if the agent is manipulated, the tool will […]
How this design can still be abused
This design prevents direct account takeover because the email is not changed without a valid verification token.
However, it can still be abused if there are no controls around how verification requests are created.
An attacker could repeatedly trigger the change_email flow for a victim account, causing the platform to send many verification emails to the legitimate account owner. The attacker still cannot comp[…]
Third design: A policy layer sits between the agent and tools
In this approach, the agent does not call privileged tools directly. Instead, all tool calls go through a policy layer. This is the approach I prefer because it creates a cleaner AI workflow instead o[…]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Policy engine sits between agent and tools
SECURITY_POLICY = {
"change_email": {"requires": "owns_account", "auto": False},
"reset_password": {"requires": "verified_token", "auto": False},
"view_orders": {"requires": "owns_account", "auto": True},
}
def execute_tool(user: str, tool: str, params: dict) -> str:
policy = SECURITY_POLICY.get(tool)
if not policy:
return "Unknown tool."
if not policy["auto"]:
# Not allowed without separate verification
token = secrets.token_urlsafe(32)
store_pending_action(user, token, tool, params)
send_email(accounts_db[user]["email"], f"Confirm: /verify?token={token}")
return f"This action requires verification. Check your email."
if policy["requires"] == "owns_account":
if not verify_account_ownership(user, params.get("username")):
return "You can only modify your own account."
# Safe action — execute directly
return TOOLS[tool](**params)
def ai_support_chat(message: str) -> str:
intent = classify_intent(message)
username, params = extract_details(message)
# Agent does not call tools directly — it goes through policy
return execute_tool(current_user, intent, params)
sequenceDiagram
participant U as User
participant AI as AI Agent
participant P as Policy Engine
participant T as Tools
U->>AI: Change my email
AI->>P: execute_tool("change_email", params)
P->>P: Policy: auto=False, requires verification
P-->>U: Token sent to verified email
U->>P: Confirm with token
P->>T: change_email(params)
T-->>U: Email updated
Note over AI: Agent has no direct tool access
This design is cleaner because it keeps authorization centralized. You are not relying on prompts, agent reasoning, or individual tool implementations to decide what is safe. The agent becomes an interface. The policy engine becomes the authority.
How this design can still be abused
This is the strongest of the three designs, but it can still fail if the policy layer is incomplete, misconfigured, or placed in the wrong order.
For example, if the policy creates a pending action or sends a verification email before checking account ownership, an attacker may still be able to trigger security emails for accounts they do not own.
Another issue is policy misclassification. If a sensitive tool is accidentally marked as safe for automatic execution, the agent may get a direct path to an action that should have required verification.
There is also a parameter confusion risk. The policy may check ownership against one parameter, while the underlying tool executes against another. For example, the policy may verify username, but the tool may use account_id or target_user internally. In that case, the policy check may pass while the tool acts on a different resource.
Maze Design
Designing an Agent is no different than designing a traditional service except that adding AI to the workflow increases the attack surface, LLMs cannot be trusted to achieve the designated goal. to be able to control LLMs you need to use what i call Maze design. When you give an agent capabilities without enforcing any policies, at a certain point it will act upon what it reasons.
flowchart TD
AGENT["AI Agent<br/>Requests: change_email"] --> G1{"GATE 1<br/>Privileged action?"}
G1 -->|No| SAFE["Safe Path<br/>Read-only / low-risk"]
SAFE --> EXEC_SAFE["Execute<br/>Normal authorization"]
G1 -->|Yes| LOCKED["Locked Path<br/>No direct execution"]
LOCKED --> G2{"GATE 2<br/>User authenticated?"}
G2 -->|No| BLOCK1["Blocked<br/>Unauthenticated request"]
G2 -->|Yes| G3{"GATE 3<br/>Owns target account?"}
G3 -->|No| BLOCK2["Blocked<br/>Ownership failed"]
G3 -->|Yes| G4{"GATE 4<br/>Tool can mutate data?"}
G4 -->|No| SAFE
G4 -->|Yes| G5{"GATE 5<br/>Policy allows action?"}
G5 -->|No| BLOCK3["Blocked<br/>Policy violation"]
G5 -->|Yes| G6{"GATE 6<br/>Rate limit passed?"}
G6 -->|No| BLOCK4["Blocked<br/>Abuse detected"]
G6 -->|Yes| STEPUP["Step-up Verification<br/>Token / MFA / verified channel"]
STEPUP --> G7{"GATE 7<br/>Verification valid?"}
G7 -->|No| BLOCK5["Blocked<br/>Invalid verification"]
G7 -->|Yes| EXECUTE["Execute Tool<br/>Action performed"]
EXECUTE --> AUDIT["Audit Log<br/>Action recorded"]
EXEC_SAFE --> AUDIT
BLOCK1 --> AUDIT
BLOCK2 --> AUDIT
BLOCK3 --> AUDIT
BLOCK4 --> AUDIT
BLOCK5 --> AUDIT
classDef agent fill:#4488ff,color:#fff,stroke:#1f4fd6,stroke-width:2px
classDef gate fill:#ff8800,color:#fff,stroke:#b85f00,stroke-width:2px
classDef safe fill:#44aa44,color:#fff,stroke:#267326,stroke-width:2px
classDef locked fill:#222,color:#fff,stroke:#777,stroke-width:2px
classDef stepup fill:#ffcc00,color:#000,stroke:#a88600,stroke-width:2px
classDef block fill:#ff4444,color:#fff,stroke:#a60000,stroke-width:2px
classDef audit fill:#666,color:#fff,stroke:#333,stroke-width:2px
class AGENT agent
class G1,G2,G3,G4,G5,G6,G7 gate
class SAFE,EXEC_SAFE,EXECUTE safe
class LOCKED locked
class STEPUP stepup
class BLOCK1,BLOCK2,BLOCK3,BLOCK4,BLOCK5 block
class AUDIT audit
Key Gates in the Maze Design:
- GATE 1 - Intent Classification: Determines if the request is a privileged action or safe operation.
- GATE 2 - Identity Verification: Verifies the user’s identity and account ownership.
- GATE 3 - Capability Scope: Checks if the tool can mutate sensitive data.
- GATE 4 - Policy Engine: Evaluates if the policy allows this specific action.
- GATE 5 - Rate Limit: Checks abuse controls and rate limiting.
Every request that reaches a “Blocked” state is logged for audit purposes. This multi-gate approach prevents agents from bypassing security controls through a single weak point.
Maze is a design pattern for forcing the agent into controlled execution paths.
What I look for when assessing agentic workflows
When I assess an agentic workflow, I start by asking where the agent is allowed to go:
- Can it call a tool directly?
- Can it mutate data?
- Can it choose the target account from user input?
- Can it trigger recovery flows for accounts the requester does not own?
- Can it send verification emails before ownership is checked?
- Can it bypass the normal application workflow because the request came through the agent?
A lot of teams treat the agent like a smart interface, but then accidentally give it backend power. So the agent is no longer just answering questions. It is making things happen. And if the tool behind it does not check ownership, the agent becomes a very polite way to hit a broken access control bug.
The real impact usually comes from what sits behind the prompt. If the agent gets tricked and nothing sensitive happens, the impact is limited. If the agent gets tricked and can touch account recovery, identity settings, payments, roles, or internal workflows, then the prompt becomes the entry point into something much bigger.
Final thoughts
What brought us here is the rush to adopt new technology without understanding the new attack surface it creates.
Organizations are adding LLMs and agents to production workflows because of FOMO. The problem is that many of these systems are being connected to sensitive actions before teams understand how they can be abused, where the trust boundaries are, and what security controls should exist around them. This is not a completely new problem. It is the same old identity and access management problem, but now it is happening with agents, tools, and non-human identities.
We already struggle with service accounts, API keys, OAuth tokens, overprivileged integrations, and broken access control. Agents add another layer to this problem because they can reason over user input and take actions through tools. That combination is dangerous when the authorization model is weak.
The industry keeps selling AI as something that solves problems, but we also need to talk honestly about the problems AI creates. If we keep connecting agents to privileged workflows without locked execution paths, we will keep seeing the same class of bugs in a new shape.
What we have seen so far is only the beginning.
