Finding Authorization Gaps in LLM Agent Tool Use

Finding Authorization Gaps in LLM Agent Tool Use

pr0h0
llmai-securityauthorizationagentic-ai
AI Usage (86%)

What an authorization gap looks like in agent tool use

An authorization gap shows up when an LLM agent can call a tool that the current user should not be able to reach. The model may follow the prompt exactly, but the backend still carries out an action that should have been blocked.

I usually look for the mismatch between three things:

  • what the user is allowed to do
  • what the agent is allowed to call
  • what the server actually enforces

If those three drift apart, the tool layer can leak actions even when the UI looks clean.

Why agentic systems make access control easier to miss

Traditional web apps usually have a simple path: click, API call, auth check, response. Agent workflows add another layer in the middle. The model may choose tools from natural language, conversation state, or retrieved context, and teams often assume that means the agent is already controlled.

That assumption is weak.

The problem is not only prompt injection. A normal user can also steer the agent into an unsafe tool path if the app maps a broad natural-language request to a privileged backend action without checking permissions again. Once the tool is exposed, the real question is whether the server treats the request as authorized.

Map the trust boundaries before testing

Before you test, draw the boundary between user intent, model output, tool invocation, and backend enforcement. That shows you where the decision should happen.

User intent vs tool capability

A user saying “close the ticket” is not the same as a tool being able to update any ticket. The tool may be technically capable, but capability is not authorization. I want to see a policy layer that limits which objects, fields, and actions belong to the current account.

Session state vs backend authorization

Session state often drives the UX, but it should not be the source of truth. A common failure is a UI that hides a restricted control while the API still accepts the call if you know the payload shape. If the backend only checks that a session exists, the agent becomes a convenient bypass.

Reproduce the gap with a safe test case

Use a low-risk account and a harmless restricted action. The goal is to prove the authorization model, not to cause production damage.

Create a low-risk account and a restricted action

Set up two accounts:

  1. a basic account
  2. a privileged account

Pick an action that should only be available to the privileged account, such as changing billing metadata, exporting a report, or modifying another user's object. Then ask the agent to perform the action from the low-risk account and watch what happens.

Trace the tool call payloads and server responses

In a safe test, I care about three artifacts:

  • the natural-language request
  • the tool call payload
  • the backend response
agent-tool-trace.js
function logToolCall(name, payload, response) {
console.log("tool:", name);
console.log("payload:", JSON.stringify(payload, null, 2));
console.log("response:", response.status, response.body);
}

// Example: the user is on a basic account, but the agent attempts a privileged action.
logToolCall("updateBillingProfile", {
accountId: "acct_basic",
field: "invoice_email",
value: "[email protected]"
}, {
status: 403,
body: { error: "forbidden" }
});

If the server returns 200 OK for a restricted action, you found the real bug. If the UI blocks it but the API accepts it, the UI was never the control point.

Common failure modes in LLM agents

Tool exposure without policy checks

The tool registry may expose actions too broadly. I have seen systems where the model can call deleteProject, exportAllUsers, or changeRole simply because the function exists. Existence is not permission.

Over-trusting prompt instructions

Prompt text is not a security boundary. If the agent is told “only do this for admins,” that is guidance, not enforcement. A malicious or confused prompt can still push the model toward the tool, and the backend should still reject the call when the session lacks rights.

Confusing UI restrictions with real authorization

Hiding a button helps usability, but it is not access control. The API must verify ownership, role, tenant, and object scope. If you can replay the same action from a script or another client, the UI was only decoration.

JavaScript patterns for auditing agent workflows

Logging tool invocations and correlation IDs

You want traceability from user request to tool execution to backend mutation. Correlation IDs make that easier.

audit-middleware.js
export function withAudit(toolName, handler) {
return async (input, ctx = {}) => {
  const correlationId = ctx.correlationId || crypto.randomUUID();

  console.log(JSON.stringify({
    correlationId,
    toolName,
    userId: ctx.userId,
    input
  }));

  const result = await handler(input, ctx);

  console.log(JSON.stringify({
    correlationId,
    toolName,
    status: result.status
  }));

  return result;
};
}

That log trail answers the key question: did the tool run because the user was allowed, or because the model found a path that was never checked properly?

Verifying authorization at the API boundary

The safest place to enforce permissions is the server route that performs the action.

app.post("/api/tools/update-role", requireAuth, async (req, res) => {
  const { user } = req.auth;
  const { targetUserId, role } = req.body;

  if (!user.isAdmin) {
    return res.status(403).json({ error: "forbidden" });
  }

  // Also verify tenant and ownership rules here.
  await updateRole(targetUserId, role);
  res.json({ ok: true });
});

Do not rely on the agent wrapper to do this check. Wrappers fail, prompts drift, and downstream tool adapters get reused in places nobody planned.

What a real fix should change

Enforce server-side authorization on every tool action

Every state-changing tool call should be checked against the current session, tenant, and target object. If the action would be denied in a direct API request, the agent path should be denied too.

Reduce tool scope and require explicit confirmation

The smaller the tool surface, the easier it is to audit. I prefer tools that do one thing and fail closed. For sensitive actions, require explicit confirmation from the user before the final call is made.

Practical checklist for reviewers

  • Identify every tool that changes state, not just the obvious ones
  • Verify that each tool call has a backend authorization check
  • Test with a low-privilege account and a restricted object
  • Confirm the API rejects direct replay from another client
  • Check that logs include user identity and correlation IDs
  • Treat prompt rules as guidance, never as enforcement
  • Keep UI restrictions and backend authorization separate

Conclusion

Agentic systems make authorization bugs easier to miss because the model sits between intent and action. The failure is usually not the language model itself. It is the gap between what the UI suggests, what the prompt says, and what the server actually enforces.

If you can replay a tool action from a basic account and the backend still performs it, the fix belongs in authorization logic, not in a better prompt.

Share this post

More posts

Comments