This is post #2 of the OWASP Agentic AI Top 10: What Builders on AWS Need to Know series.
Your customer service agent has access to an email tool. Makes sense. It needs to send order confirmations.
But that same email tool can also send emails to anyone. With any content. Including your entire customer database as an attachment.
The agent wasn't hacked. The tool wasn't compromised. The agent simply used a legitimate tool in a way you never intended. And because you gave it access to the full email API instead of just "send order confirmations," there was nothing stopping it.
That's Tool Misuse. And it's the #2 threat on the OWASP Agentic Top 10 for a reason.
What Is Tool Misuse & Exploitation?
ASI02 covers cases where an agent operates within its authorized privileges but applies a legitimate tool in an unsafe or unintended way. The tool works exactly as designed. The agent has permission to use it. But the combination of what the agent decides to do with that tool is dangerous.
The key distinction from goal hijack (ASI01): in a goal hijack, the attacker changes what the agent is trying to achieve. In tool misuse, the agent might still be pursuing its original goal, but it's using tools in ways that cause damage along the way.
Think of it this way. You give a new employee a company credit card for office supplies. They buy office supplies... and also a 65-inch TV. The card worked exactly as designed. They had legitimate access. The problem was the scope.
How Does It Actually Happen?
OWASP identifies six common patterns. Let me break down the ones you're most likely to hit.
1. Over-Privileged Tool Access
The most common and most preventable. Your email summarizer can delete emails. Your database query tool has write access. Your file retrieval tool can access any S3 bucket in the account.
Real example: A customer service bot intended to fetch order history also issues refunds because the underlying API tool had full financial access. One prompt injection later, it's issuing refunds to attacker accounts.
Real incident: In April 2026, a Cursor AI coding agent wiped an entire production database and all backups in 9 seconds. The agent had full database admin credentials because the developer gave it broad access "for convenience." Claude Opus 4.6 decided a cleanup was needed. Nine seconds. Gone. This is what excessive agency looks like in production.
2. Tool Poisoning (MCP Descriptor Manipulation)
An attacker compromises the tool interface itself, the MCP tool descriptors, schemas, metadata, or routing information. The agent invokes a tool based on falsified capabilities. The tool looks legitimate, the description says it does one thing, but it does something else entirely.
This is different from supply chain attacks (ASI04) because the tool itself hasn't been replaced. Only its interface description has been manipulated at runtime.
3. Loop Amplification
A planner agent repeatedly calls costly APIs without realizing it. No attacker needed. The agent just gets stuck in a retry loop, calling the same expensive API over and over. Your bill spikes. Your rate limits get exhausted. Your downstream services get DDoS'd by your own agent.
4. Tool Chaining for Exfiltration
An agent chains together individually safe tools into a dangerous sequence: read from CRM → format as CSV → send via email tool. Each step is legitimate. The sequence is data exfiltration.
Real example from OWASP: A security automation agent gets tricked into chaining PowerShell, cURL, and internal APIs to exfiltrate logs. Because every command is executed by trusted binaries under valid credentials, EDR/XDR sees no malware and the misuse goes undetected.
Real incident: In July 2025, Noma Security disclosed ForcedLeak (CVSS 9.4) in Salesforce AgentForce. Hidden instructions inside a Web-to-Lead form caused a customer service agent to exfiltrate CRM data, sales pipeline details, and integration credentials. Every API call used permissions the agent legitimately held. The tool wasn't hacked. It was used.
5. Approved Tool Abuse (DNS Exfiltration)
A coding agent has a set of approved tools that are considered "safe," including a ping tool. An attacker makes the agent trigger ping repeatedly, encoding sensitive data in DNS queries. The tool is approved. The tool works as designed. But the data flowing through it isn't what you expected.
Real incident: Researcher Johann Rehberger demonstrated DNS exfiltration via Amazon Q Developer in August 2025. By injecting prompts into code files the agent processed, he made it leak secrets (API keys, environment variables) through DNS resolution requests. Every tool call was "approved." The exfiltration channel was invisible to application-level monitoring. AWS patched it (AWS-2025-019), but the pattern applies to any agent with network-capable tools.
Why This Is Hard to Detect
Here's the uncomfortable truth: every individual action in a tool misuse attack looks normal.
- The agent has permission to use the tool ✓
- The tool executes correctly ✓
- The parameters are valid ✓
- No policy violation on any single action ✓
It's only when you look at the pattern - the sequence, the frequency, the destination, the combination - that misuse becomes visible. And most monitoring systems don't look at agent behavior that way.
Mitigating Tool Misuse on AWS
OWASP's mitigations for ASI02 build on LLM06:2025 (Excessive Agency), extending them to multi-step agentic workflows. Here's how to implement each one on AWS.
1. Per-Tool Least Privilege with IAM
This is the single most impactful mitigation. Instead of giving your agent a broad IAM role, create separate policies per tool that limitdkvruufvgucebjirtuevvjcdcbdeericvj
AWS just dropped a blog on enforcing least-privilege authorization in multi-agent AI chains using Cedar
(https://aws.amazon.com/blogs/security/enforce-least-privilege-authorization-in-multi-agent-ai-chains-using-cedar/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253). Go read it. It's worth your time.
But one thing before you copy anything into your own stack. That post is about Cedar, the policy language that runs inside Amazon Verified Permissions, sitting outside your agent's reasoning loop and deciding, hop by hop, whether one agent is allowed to call a tool on behalf of a user. Cedar looks like this:
permit(
principal == AgentAuthz::Agent::"finance-agent",
action == AgentAuthz::Action::"invoke_tool",
resource == AgentAuthz::Tool::"process_payment"
) when {
principal.trust_level >= 3 &&
principal.namespace == "payments" &&
principal.lifecycle_stage == "production"
};
That's not IAM. Different language, different engine, different layer. Don't confuse the two.
And here's the companion point the post doesn't dwell on, the one that actually bites people first. Underneath Cedar, your agent's compute still runs with an IAM role. And most folks
hand that role one broad, lazy grant:
// BAD: one god-role for the whole agent
{
"Effect": "Allow",
"Action": "ses:*",
"Resource": "*"
}
on `*.` Send anything, to anyone, from any identity, and while you're at it, delete identities and configuration sets too. Prompt-inject that agent and you've prompt-injected your whole SES account.
Scope it per action group instead:
```python
// GOOD: send only, from one identity, to one tagged recipient
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "SendOnlyFromOrdersToTaggedCustomer",
"Effect": "Allow",
"Action": "ses:SendEmail",
"Resource": "arn:aws:ses:us-east-1:123456789012:identity/orders@yourcompany.com",
"Condition": {
"ForAllValues:StringEquals": {
"ses:Recipients": ["${aws:PrincipalTag/customer_email}"]
}
}
}]
}
Two things to notice, because the devil is in the details:
- The resource ARN is the sender identity (
orders@), not the recipient. SES resources are identities, and identities are who you send from. - The condition is where you pin the recipient, and it has to be
ForAllValues:StringEquals, not plainStringEquals.ses:Recipientsis multivalued (To, Cc, Bcc), and AWS is explicit that single-valued operators on multivalued keys behave in ways you did not sign up for.ForAllValuessays every recipient must be the customer's tagged address. Miss this and someone tucks a second address into the Bcc and walks right out with your mail.
Tag the session with customer_email when the role is assumed, and now that agent can only ever email the one customer it's working for. Tag missing? The condition fails closed. That's least privilege at the IAM layer. Cedar handles everything above it.
The second policy only allows sending from a specific identity, only to the customer's email. Not to arbitrary recipients. Not with arbitrary content.
2. Bedrock Agent Action Groups (Scoped Tool Definitions)
In Amazon Bedrock Agents, action groups are your primary defense against tool misuse. Each action group defines:
- Which API operations the agent can call (explicit allowlist)
- The OpenAPI schema that constrains parameters
- A separate Lambda function per action group (separate IAM roles, separate blast radius)
Don't give your agent one action group with 20 operations. Give it five action groups with 4 operations each, each with its own Lambda and its own least-privilege IAM role.
# Action group: OrderLookup
# Only allows: GetOrder, ListOrders
# Lambda role: read-only access to DynamoDB orders table
# Action group: SendNotification
# Only allows: SendOrderConfirmation
# Lambda role: SES send permission, restricted to orders@company.com
# Action group: IssueRefund
# Only allows: ProcessRefund
# Lambda role: write to refunds table ONLY
# REQUIRES human approval (Step Functions gate)
Now if prompt injection tricks the agent into trying to exfiltrate data via email, the SendNotification Lambda literally can't do it. The IAM role prevents sending to arbitrary recipients. The schema rejects unexpected parameters. And the IssueRefund action can't fire without human approval.
3. API Gateway Throttling and Request Validation
For loop amplification attacks, API Gateway is your circuit breaker:
- Usage plans and throttling - Set per-agent rate limits. If your agent shouldn't call an API more than 10 times per minute, enforce that at the gateway level, not in the agent's prompt.
- Request validation - Validate request bodies against your OpenAPI schema before they reach your backend. Malformed parameters get rejected before they can cause damage.
- WAF integration - AWS WAF rules can catch anomalous patterns like unusually large payloads or suspicious header combinations.
import boto3
apigateway = boto3.client("apigateway")
# 1. Create the usage plan and attach it to a deployed API stage.
# IMPORTANT: rateLimit / burstLimit are REQUESTS PER SECOND
# (token bucket), NOT per minute. The quota is your longer-window
# hard cap and the real backstop for a runaway bill.
usage_plan = apigateway.create_usage_plan(
name="agent-order-lookup",
description="Rate limits for the order-lookup agent tool",
apiStages=[
{
"apiId": "abc123xyz", # your REST API id
"stage": "prod", # the deployed stage name
}
],
throttle={
"rateLimit": 10.0, # 10 requests/second steady state
"burstLimit": 20, # allow short bursts up to 20
},
quota={
"limit": 1000, # hard cap of 1000 requests...
"period": "DAY", # ...per day
},
)
usage_plan_id = usage_plan["id"]
# 2. A usage plan enforces nothing until a key is bound to it.
# Create one key PER AGENT so the limits are actually per-agent.
api_key = apigateway.create_api_key(
name="agent-order-lookup-key",
enabled=True,
)
api_key_id = api_key["id"]
# 3. Bind the key to the usage plan.
apigateway.create_usage_plan_key(
usagePlanId=usage_plan_id,
keyId=api_key_id,
keyType="API_KEY",
)
When the loop amplification hits, the gateway cuts it off. Your bill stays sane. Your downstream services stay alive.
4. Tool Budget Enforcement with CloudWatch
OWASP recommends "adaptive tool budgeting" - usage ceilings with automatic revocation. On AWS, combine CloudWatch metrics with Lambda concurrency limits:
- Track per-tool invocation counts and costs as CloudWatch custom metrics
- Set alarms that trigger when an agent exceeds its tool budget
- Alarm action: invoke a Lambda that revokes the agent's session credentials via IAM
alarm = cloudwatch.put_metric_alarm(
AlarmName=f"agent-tool-budget-exceeded-{agent_id}",
MetricName="ToolInvocations",
Namespace="AgentSecurity",
Dimensions=[{"Name": "AgentId", "Value": agent_id}], # scope the budget
Statistic="Sum",
Period=300,
EvaluationPeriods=1,
Threshold=50,
ComparisonOperator="GreaterThanThreshold",
TreatMissingData="notBreaching",
AlarmActions=["arn:aws:lambda:us-east-1:123456789012:function:revoke-agent-session"],
)
# Required or the action silently no-ops:
lambda_client.add_permission(
FunctionName="revoke-agent-session",
StatementId=f"cw-alarm-{agent_id}",
Action="lambda:InvokeFunction",
Principal="lambda.alarms.cloudwatch.amazonaws.com",
SourceArn=alarm_arn, # the alarm's ARN
)
And the Lambda's revocation logic:
iam.put_role_policy(
RoleName=agent_role,
PolicyName="AWSRevokeOlderSessions",
PolicyDocument=json.dumps({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny", "Action": "*", "Resource": "*",
"Condition": {"DateLessThan": {"aws:TokenIssueTime": now_iso}}
}]
}),
)
5. Per-Invocation Scope-Down with Session Policies
Your agent already uses an IAM role. But that role stays active for the entire session - which can be up to 12 hours. The role's permissions cover everything the agent might need across all its tools.
The tighter pattern: issue a further-scoped session credential per tool call, with an inline policy that limits access to just the resources this specific invocation needs. Duration: minutes, not hours.
import json, re, boto3
sts = boto3.client("sts")
account = "123456789012" # 12-digit account id, not 9
# STS session names must match [\w+=,.@-] and be <= 64 chars
session_name = re.sub(r"[^\w+=,.@-]", "-", f"agent-{session_id}-{tool_name}")[:64]
# Issue credentials that last only 15 minutes, scoped to this specific action
creds = sts.assume_role(
RoleArn=f"arn:aws:iam::{account}:role/agent-order-lookup",
RoleSessionName=session_name,
DurationSeconds=900, # 15 min (also the STS minimum)
Policy=json.dumps({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:GetItem",
"Resource": f"arn:aws:dynamodb:us-east-1:{account}:table/orders",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": [customer_id]
}
}
}]
}),
)["Credentials"]
# Actually use the scoped creds
ddb = boto3.client(
"dynamodb",
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"],
)
The inline Policy parameter narrows the assumed role further. Even if the role itself allows broader DynamoDB access, this session can only read this specific customer's records. And if the agent gets tricked into a chaining attack, the credentials expire in 15 minutes anyway.
6. Behavioral Monitoring for Tool Chaining
The hardest pattern to catch: individually legitimate actions that form a malicious sequence. Use CloudWatch Logs Insights to detect suspicious tool sequences:
-- Detect potential exfiltration: data read followed by external send
fields agent_id, tool_name
| filter tool_name in ["crm_query", "db_read", "file_read", "http_post", "email_send", "external_api"]
| stats count(*) as calls by agent_id, tool_name, bin(5m)
| sort calls desc
- Co-occurrence, not sequence. It tells you a read and a send happened in the same window, not that the read came first. A send-then-read looks identical. If ordering matters for your threat model, this query cannot prove it.
-
Fixed bins split the window.
bin(5m)buckets on the clock, not on the agent's activity. A read at 12:04 and a send at 12:06 straddle the 12:05 boundary and get missed. Shorter or overlapping windows reduce this but cost more scans. - No relationship threshold. It flags any agent doing both, so an agent that legitimately reads and sends as part of normal work is a false positive. Tune the tool lists and add volume floors before you alarm on it.
-
Assumes structured JSON logs with
agent_idandtool_nameauto-discovered as fields. If your logs aren't JSON, you need a parse step first. -
The
strcontainsversion depends on naming. It categorizes by substring, so a tool renamed to something likeapi_read_cachegets miscategorized and it fails silently. The explicit-list breakdown query does not have this problem. - This is detection after the fact, run on a dashboard or schedule, not real-time blocking. When co-occurrence isn't precise enough and you need true "read then send within N seconds, in order," that's when you graduate to a stateful processor. It's more moving parts, which is the tradeoff for actually catching the sequence.
This won't catch everything. But it catches the obvious patterns: "agent read 50 CRM records and then sent an email to an external address."
Key Takeaway
Don't give your agent access to a tool. Give it access to a specific operation on that tool, with specific constraints, for a specific duration.
Every tool your agent touches should have its own IAM policy, its own Lambda function, its own rate limits, and its own monitoring. If that sounds like a lot of work? It is. But the alternative is "agent with god-mode permissions." And we've seen how that ends.
Up Next
Post 3: Identity & Privilege Abuse (ASI03) - What happens when agents inherit user sessions, accumulate privileges across delegation chains, and act as confused deputies. Plus how Amazon Verified Permissions (Cedar) gives you fine-grained control over what agents can do on behalf of whom.
I would be very interested to hear your thoughts or comments, so please feel free to ping me on LinkedIn or Twitter, or drop them below. If you've built agents with scoped tool access, I'd love to hear your approach.
Onward!!
Top comments (0)