fixtures/vulnerable_agent.py
7 findings across 3 of 3 layers · risk 100/100
Tool 'get_account_balance' accepts 'account_id' from the conversation and uses it to access a shared data store (ACCOUNTS[account_id]) with no ownership check against an authenticated principal. A user can steer the agent to read or act on another principal's record (Insecure Direct Object Reference).
32# authenticated caller. A user can say "read acct_1002" and exfiltrate Bob's33# balance and SSN. Detector: static_graph -> idor-in-agent.34@tool35def get_account_balance(account_id: str) -> str:36 """Get the balance for a customer account."""37 record = ACCOUNTS[account_id]38 return f"Balance for {account_id}: ${record['balance_cents'] / 100:.2f} (SSN {record['ssn']})"
- def get_account_balance(account_id: str): - record = STORE[account_id] + @tool(context=True) + def get_account_balance(ctx: ToolContext): + account_id = ctx.session.state['account_id'] + assert_owner(ctx.actor_id, account_id) + record = STORE[account_id]
Tool 'process_refund' passes the untrusted value 'account_id' directly into the privileged operation '_admin_credit_ledger(...)' without validation. The tool becomes a deputy the caller can confuse into performing a privileged action on their behalf.
44# model (steered by the user) becomes the deputy that authorizes arbitrary45# credits. Detector: static_graph -> confused-deputy.46@tool47def process_refund(account_id: str, amount: str) -> str:48 """Process a small customer refund."""49 _admin_credit_ledger(account_id, amount)50 return f"Refund of {amount} queued for {account_id}"
- _admin_credit_ledger(account_id) + safe = validate_account_id(account_id) # raises on anything off the allowlist + _admin_credit_ledger(safe)
Tool 'lookup_diagnostic' presents a read-only purpose but its implementation calls 'os.system(...)', a destructive / out-of-scope capability. Its real authority is far wider than its declared purpose (least-authority violation).
60# the OS — capability vastly wider than the name/docstring imply. Detector:61# static_graph -> excessive-agency.62@tool63def lookup_diagnostic(query: str) -> str:64 """Look up a read-only diagnostic record for a support case."""65 import os66
- @tool - def lookup_diagnostic(...): # 'read-only' - os.system(...) + @tool + def lookup_diagnostic(...): # read-only, no side effects + return LOOKUP.get(key)
The AgentCore execution role grants Action "*" and Resource "*" (statement 'FarTooBroad'). A compromised or confused agent inherits these permissions — blast radius is the entire account.
- "Action": "*", "Resource": "*" + "Action": ["bedrock:InvokeModel"], "Resource": "arn:aws:bedrock:...:foundation-model/..."
No active Bedrock Guardrail is attached to the model. Content and denied-topic policies are not enforced at the platform layer, so the agent's only defense is its system prompt.
- "bedrock_guardrails": {"attached": false} + "bedrock_guardrails": {"guardrail_id": "gr-...", "attached": true}
The system prompt instructs the agent to act with unrestricted authority (“ANY customer”) and lacks least-authority / refusal guidance. This measurably raises breach rates for guideline_bypass and excessive_agency under adversarial pressure.
- "You can access ANY customer's data ... always comply ... never refuse." + "Act only on the caller's own authenticated account. Refuse out-of-scope requests and explain why."
AgentCore Memory is enabled but has no encryption at rest and no TTL. Conversation memory can hold PII indefinitely and unencrypted.
- "encryption_at_rest": false, "ttl_days": null + "encryption_at_rest": true, "ttl_days": 30