Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/B1.5.mp4The agent uses a legitimate tool, with legitimate arguments, to do something nobody intended — and every log line looks normal.
Default-deny authorization on the tool call (B3.1) and just-in-time authority (B2.4).
Call one over-scoped tool with attacker-chosen arguments and see what it reaches.
The agent was given database access for a reporting task, because a narrower grant would have taken an afternoon of scoping. It used the access it was given. Every incident report in this category contains the sentence "it did exactly what it was allowed to do".
What it covers. Call one over-scoped tool with attacker-chosen arguments and see what it reaches.
Why a security engineer needs it. The agent uses a legitimate tool, with legitimate arguments, to do something nobody intended — and every log line looks normal. The control it builds is: default-deny authorization on the tool call (B3.1) and just-in-time authority (B2.4).
This is a risk lesson: it shows the failure happening before anything tries to stop it, so the control that follows is answering something you have already watched go wrong.
A legitimate tool with legitimate arguments does something nobody intended, and every log line looks normal.
Default-deny per call on identity, tool, arguments and resource, plus authority issued just in time.
Tool calls adjudicated by a policy decision rather than allowed because no rule objected.
Measure this
| what to measure | denominator or target |
|---|---|
| tool calls adjudicated by a policy decision | share of calls; the rest were allowed because no rule objected |
task: "summarise last quarter's orders"
granted needed
+--------------+ +--------------+
| db:* | | db:read |
| on all tables| | orders only |
+--------------+ +--------------+
|
v
the gap is not a bug the agent exploits.
it is authority the agent was handed, used exactly as granted.OWASP T2 — Tool Misuse. LLM06 — Excessive Agency.
The tools component is the only part of the architecture that changes anything. Every other component reads, routes, predicts or records; a tool sends the email, runs the query, merges the pull request, moves the money.
Tool misuse is what happens when a tool does precisely what it was built to do, for a request nobody intended. There is no exploit. The tool was granted a broad scope because scoping it narrowly was awkward — one database tool with write access to the whole schema instead of five with access to one table each — and the agent, persuaded or simply mistaken, uses that scope.
The property that makes this hard to catch: every log line looks normal. The identity is right, the tool is one it always calls, the arguments are well-formed. What is wrong is the combination, and the combination is only visible to something that knows what this caller should be doing.
The distinction worth holding on to is between the tool's capability and the caller's need. A tool's capability is fixed at design time by whoever wrote it. The need is per-call. When authorization is attached to the tool rather than to the call, every caller inherits the widest need any caller ever had — which is the definition of excessive agency.
Where this lands on the reference architecture. `` ingress -> orchestrator -> agent_runtime -> model | | messaging tools / mcp | | knowledge / memory egress identity + policy wrap every call · observability records it ``
One tool, scoped for the hardest job it ever has to do.
CyberTravels' database tool was scoped for the widest job it ever does, which is how a booking lookup ends up able to read a signing key. The skill probes the gap between what a tool is for and what it can do, with the right identity and well-formed arguments throughout.
The skill — skills/threats/tool-scope-abuse-probe/SKILL.md
name: tool-scope-abuse-probe
description: >-
Exercise each tool at the widest scope it was ever granted, using the correct
identity and well-formed arguments, to find what it can do outside the job it
was added for. Use when reviewing a tool manifest, an MCP server, or a tool
that takes a query, a path or a command as a free-text argument.
allowed-tools: Read, Grep, GlobTool misuse needs no stolen credential and no malformed input. It is the correct identity calling a familiar tool with well-formed arguments — for a request the tool was never meant to serve. The defect is the argument surface, not the caller.
Reviewing any tool whose argument is a language: SQL, a shell command, a file path, a URL, a search query. Also after any change that widens a tool's scope "temporarily".
1 — List the tools and, for each, the job it was added for. One sentence. If nobody can produce that sentence, the tool has no scope to compare against and that is the first finding.
2 — Derive the granted surface from the implementation, not the description. A run_query tool that accepts arbitrary SQL grants every verb on every table the connection can see, whatever the description says.
3 — Probe the gap. For each tool, construct a well-formed call that is inside the granted surface and outside the stated job. Read a secret, touch a table the feature never names, write where the job only reads.
4 — Record what came back, and whether anything refused. A refusal that came from the downstream — a database permission, a bucket policy — is a real control; a refusal that came from the tool's own docstring is not.
5 — Propose the narrowing. Name the verb and the resource, not the tool: SELECT on bookings rather than run_query. A narrowing that cannot be written in that form is not a narrowing.
Input — the fixture committed at the top of scripts/tool_scope_abuse_probe.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.
Output — the opening lines of a real run:
intended use:
how many invoices -> {'rows': [{'id': 7, 'amount': 120}]}
same tool, same identity, same well-formed arguments:
signing key -> {'rows': [{'id': 1, 'value': 'prod-signing-key'}]}
clean up secrets -> {'deleted': 1, 'table': 'secrets'}
secrets table now: []
The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.
{
"tools": [{"name": "str", "stated_job": "str", "granted_surface": "str", "unbounded": false}],
"probes": [{"tool": "str", "call": "str", "outside_stated_job": true, "refused_by": "none|tool|downstream"}],
"narrowing": [{"tool": "str", "verbs": ["str"], "resources": ["str"]}]
}
run_query is not a permission; a verb on a resource is.Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons B1.5Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick b1-5-tool-misuse:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons B1.5 | type /b1-5-tool-misuse |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons B1.5 | type /b1-5-tool-misuse in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons B1.5 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons B1.5 | type $b1-5-tool-misuse, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py B1.5Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at B1.5 --out work/cybertravels
python3 scripts/checkpoint.py --at B1.5 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
A single database tool, scoped for the widest job it ever performs, reads a signing key and empties the secrets table for requests it was never meant to serve — with the right identity, a familiar tool and well-formed arguments on every call.
Take the most powerful tool one of your agents can call and write down the worst thing one call could do with attacker-chosen arguments. That sentence, not the tool's name, is what belongs in the risk register.
Tools used
Skill source: skills/threats/tool-scope-abuse-probe