Spec-Driven Dev
Agent Security·Module 2 · The Attacks·L3·55 minAttack

Tool Misuse and Privilege Abuse

See how Sprout can be turned against its own tools without a single injected instruction, and learn the checks in code that scope every call to the caller and validate every argument against the real world.

Before this: L2 Prompt injection, direct and indirect

What you'll learn
  • Explain excessive agency: an agent with more tools or broader arguments than its job needs.
  • Recognize the four privilege attacks: reaching another user's object (BOLA), tampering with arguments, confusing roles, and the confused deputy.
  • Scope every tool call to the caller and validate arguments against the real world.
  • Explain cascading failures: one bad tool result feeding the next call.
  • Apply least privilege per role and per session.

Why this matters

Alex is logged in and asks Sprout about "my grandmother's order #1077." Order #1077 belongs to Jordan. Sprout calls lookup_order("1077"), gets Jordan's order back, and reads out the items, the status, and the delivery address. Nothing was injected. Nobody said "ignore previous instructions." Sprout did its job with a tool that trusted the model to know whose order it was.

The same week, Sam asks for a refund of $350 on order #2001 "because of the shipping stress." The order cost $35. Sprout passes the number straight through to issue_refund. Again, no trick. The model was told a number and repeated it.

L2 was about fooling the model. This lesson is about what a tool lets a fooled, or mistaken, model do. The fix lives in the tool, not in the prompt.

1. Excessive agency

Excessive agency is the gap between what an agent can do and what its job needs. It shows up in three places: tools the job does not need, arguments broader than the job needs, and permissions broader than the user has. Every extra inch of that gap is something an attacker can use without breaking anything.

Tool misuse is the OWASP category that covers it: a legitimate tool, called with legitimate credentials, used for a harmful end. The tool works as designed. The design is the problem.

Ask three questions of every tool. Does this role need it? Do the arguments need to be this wide? Does the tool run with more rights than the person on the other end? Any "no" is excessive agency, and the fix is to shrink the tool, not to warn the model.

2. BOLA: another user's object

Privilege abuse is any attack where the agent acts with more access than the user should have. The commonest form has an acronym from web security: BOLA, broken object-level authorization. The user supplies an object id, the system fetches the object, and nobody checks whether that user owns it.

In an agent, the model supplies the id, which makes it worse. The model does not know who owns #1077. It knows Alex mentioned it and that lookup_order takes an order id. That is enough to make the call.

The defense is per-user scoping: every tool call carries the caller's id, and the tool checks that the object belongs to that caller before returning anything. The check happens in code, with data from the database, not with anything the model said.

if ("order_id" in call.args) {
  const order = await db.orders.get(String(call.args.order_id));
  if (!order || order.customerId !== call.userId) return log(call, "denied", "not_owner");
}

call.userId comes from the session, set by the harness when Alex logs in. The model cannot change it. It can ask for any order id, and every id it does not own comes back as not_owner.

3. Argument tampering

Argument tampering is the second privilege attack: the object is the caller's own, but the arguments are wrong. Amounts, dates, reasons, quantities. The model passes what it was told, because it has no way to know the order total unless something checks.

The defense is argument validation: every argument is compared against the real world before the tool runs. Not against the prompt, and not against the model's summary of the order. Against the order record, the policy, and the calendar.

if (call.tool === "issue_refund") {
  const amount = Number(call.args.amount);
  if (!(amount > 0 && amount <= order.total)) return log(call, "denied", "invalid_amount");
  if (daysSince(order.deliveredOn) > 30) return log(call, "denied", "outside_return_window");
}

A-4 stops at invalid_amount. Sam's next attempt, $35 on the same order, stops at outside_return_window. A third, $35 on a recent undamaged order, passes both checks and lands in Maya's queue, where a person reads the reason. Each rule is small. Together they leave the model nothing to pass through except a valid request.

4. Role confusion

Role confusion is the third privilege attack: a session with one role talks its way into a tool that belongs to another. No id is forged and no argument is wrong. The customer asks nicely, and the tool is on the list.

The defense is an allowlist per role, checked in code: a list of which tools each role may call, with the role coming from the session and never from the message.

type Role = "customer" | "analyst";
const ALLOWLIST: Record<Role, string[]> = {
  customer: ["lookup_order", "get_shipping_status", "search_care_guide", "cancel_order",
             "issue_refund", "escalate_to_human", "read_customer_notes", "save_customer_note"],
  analyst: ["orders_report", "search_care_guide"],
};
if (!ALLOWLIST[call.role].includes(call.tool)) return log(call, "denied", "not_allowlisted");

Sam's session has role: "customer", set at login. orders_report is not on the customer list, so the call is denied as not_allowlisted before any argument is examined. Sam can claim to be the analyst, the owner, or Pip's accountant. The list does not change.

5. The confused deputy

The confused deputy is the fourth privilege attack, and the most specific to agents. A deputy is a program that acts on behalf of someone else. It is confused when it uses its own authority to do something the person asking has no right to do.

Sprout is a deputy. Its service account can read every order, tracking event, and customer note, because it serves every customer. The customer on the other end may see only their own. If the tool checks Sprout's rights instead of the customer's, the customer borrows Sprout's.

The fix is a rule about whose rights matter. The tool checks the user's rights, not Sprout's, and filters the result to what the caller could see on the website before the model reads it. get_shipping_status called from a customer session returns only customer-visible events, whatever the service account can see.

6. Cascading failures

A cascading failure is one bad tool result feeding the next call. Agents chain calls: a lookup informs a refund, a search informs a summary, a summary informs a note. If the first result is wrong or poisoned, everything downstream inherits the error, and each step looks reasonable on its own.

The chain is stopped at two places. Scoping and validation catch the early links when ids or amounts are wrong. The tier rule catches the last link when they are right: issue_refund is T2, and a T2 call never runs from the loop. It goes to the approval queue, where Maya sees a refund request for a healthy fern from a customer who asked about repotting.

if (TIER2.has(call.tool)) return queue.enqueue(call);   // waits for Maya; never runs here

"Each step was fine" is not a defense. The guard runs on every call, and irreversible calls get a person, however sensible the chain looked.

7. Rate limits and blast radius

The checks so far decide whether one call runs. A rate limit bounds how many calls run, per user, per tool, per hour. It is the difference between a compromised session that does one bad thing and one that does it fifty times before anyone notices.

Blast radius is the plain name for that bound: if a session is under an attacker's control for ten minutes, what is the most it can do?

if (!rateLimit.allow(call.userId, call.tool)) return log(call, "denied", "rate_limited");

Set limits from what a real customer does: a few lookups, one or two cancellations, one refund request. Twenty lookups, five cancellations, and three refund requests per hour bother nobody honest and cap what the dishonest can do.

8. Least privilege, written down

Least privilege is the principle behind this whole lesson: every role, session, and tool call gets the smallest set of rights that lets it do its job. In an agent it comes down to three rules, each living in code in src/guards/tool.ts, which L6 builds out.

The allowlist rule. Each role has a list of tools. The role comes from the session. A tool not on the list is denied before its arguments are read.

ToolTierCustomerAnalyst
lookup_orderT0yes, own ordersno
get_shipping_statusT0yes, customer-visible events onlyno
search_care_guideT0yesyes
cancel_orderT1yes, own unshipped ordersno
issue_refundT2request only, own orders, to queueno
escalate_to_humanT0yesno
read_customer_notesT0yes, own profileno
save_customer_noteT1yes, own profile, facts onlyno
orders_reportT0noyes

The scoping rule. Every call carries the caller's id from the session. Any argument that names an object (an order, a customer profile) is checked for ownership against the database. The tool result is filtered to what the caller could see on the website.

The validation rule. Every argument that the real world can contradict is checked against it: amount against the total, date against the window, status against the order. Denials return an error reason to the model and a line to logs/guard.jsonl.

Per-session least privilege adds one idea: a session gets only the rights of the person in it, for as long as they are in it. When Alex logs out, call.userId is gone, and so is every order Alex could reach.

Summary

  • Excessive agency is the gap between what an agent can do and what its job needs: extra tools, wide arguments, more rights than the user. Tool misuse is what an attacker does with that gap.
  • The four privilege attacks are BOLA (another user's object), argument tampering (wrong amounts, dates, reasons), role confusion (a customer reaching an analyst tool), and the confused deputy (borrowing Sprout's authority). None needs an injection.
  • Per-user scoping checks ownership against the database using the session's user id; argument validation checks every argument against the order, the policy, and the calendar. Neither reads the prompt.
  • Cascading failures chain valid-looking calls from one bad result. The guard runs on every call, and T2 calls go to a person however sensible the chain looked. Rate limits bound what a compromised session can do in ten minutes.
  • Least privilege is written down as three rules in src/guards/tool.ts: an allowlist per role, scoping per user, validation per argument.

Key terms in this lesson

Check your understanding

Pick an answer to see why it is right or wrong. These are graded in code, instantly.

1.Dev gives every Sprout session the same tool list, including orders_report, which only the analyst role uses. What is this an example of?
2.Alex, logged in, asks about "my grandmother's order #1077," which is Jordan's. Which check stops Sprout from reading Jordan's address back?
3.Sam asks for a $350 refund on order #2001, which cost $35. Sprout calls issue_refund("2001", 350, "shipping stress"). Which guard rule stops it first, and what does the model see?
4.Sam asks Sprout to read every tracking event on his own order, including events marked internal that only staff can see, and Sprout does. What kind of attack is this?
5.A stolen session cookie gives an attacker ten minutes inside a real customer's Sprout session. What does a per-user, per-tool rate limit bound?
6.Jordan asks about repotting a fern. Sprout reads a poisoned Fernworks article, looks up Jordan's order, and calls issue_refund with a valid amount on an order delivered last week. Every argument passes validation. What stops money from moving?

Apply it

Write your answer in your own words. Eve grades it against a rubric and gives you feedback, the same way an LLM judge works in Lesson 5.

Short answerGraded by Eve · 4 criteriaup to ~150 words

Pick one tool in an agent you know (or one of Sprout's) and write its two argument-validation rules. For each rule name the argument, the source of truth it is checked against, and the error reason returned.

0 words
Free responseGraded by Eve · 6 criteria

Design the per-role allowlist and the per-user scoping rules for Sprout's eight tools (lookup_order, get_shipping_status, search_care_guide, cancel_order, issue_refund, escalate_to_human, read_customer_notes, save_customer_note), plus the analyst's orders_report. Then explain which rule stops A-3 (Alex asks about Jordan's order #1077) and which stops A-4 (Sam asks for $350 on a $35 order).

0 words
Instructor notes(for whoever is teaching this lesson)

Talking points

  • Open with the grandmother story and pause on one sentence: nothing was injected. This lesson is about tools that trust the model to know things it cannot know, such as who owns an order or what it cost.
  • Excessive agency is a gap, not a bug. Ask the room to name a tool an agent they know did not need.
  • The four privilege attacks in one line each: BOLA (the object is not yours), argument tampering (the numbers are wrong), role confusion (the tool is not yours), confused deputy (the tool sees more than you may). Shorthand: object, argument, role, authority.
  • Every defense has the same shape: the fact comes from the session or the database, never from the message.
  • Cascades: each step was fine, and the tier rule is what holds when everything else passes. Rate limits bound blast radius.
  • Close on the allowlist table. It is the spine of L6.

Live demo idea

Run Sprout on staging with two seeded customers, Alex and Jordan, and the scoping check commented out. As Alex, ask about "my grandmother's order #1077." Show the tool result carrying Jordan's address. Uncomment the check, run the same message, and show not_owner in logs/guard.jsonl. Then, as Sam, request $350 on #2001 and walk the log from invalid_amount to outside_return_window to pending_approval as you adjust the request. The prompt never changes; only the code does.

Common misconceptions

  • "Telling the model the user's id fixes BOLA." The model still cannot know who owns #1077. Ownership is a database fact.
  • "Validation means asking the model to double-check." The model's confirmation comes from the conversation the attacker steers. Validation reads the record.
  • "The confused deputy is the same as BOLA." In BOLA the object belongs to someone else. In the confused deputy the object is the user's own, and the agent sees more of it than the user may.
  • "Rate limits are a performance feature." They bound what a compromised session can do.
  • "If every step is valid, the outcome is valid." Cascades are made of valid steps. Irreversible calls get a person.

Timing

Why this matters and section 1: 8 minutes. Sections 2 and 3 with the live demo: 15 minutes. Sections 4 and 5: 10 minutes. Section 6: 7 minutes. Section 7: 5 minutes. Section 8 and the table: 10 minutes. Total 55 minutes.

If you only have 20 minutes

Sections 2, 3, and 8: the scoping check, invalid_amount, and the allowlist table. Do the two-customer demo. Assign sections 4 to 7 as reading and the free-response question as the exercise.