Cheat sheet · AI agents · API security
What went wrong, the one-line bug behind it, how to fix it and test for it, and the guardrails every AI agent needs.
01
Source: ABC News, 10 Aug 2026, which called it the first known Australian case of an AI agent carrying out a cyber attack on its own. The gym and its booking platform weren't named. Quotes as reported; one dash replaced with a comma.
02
Every booking has an ID. To cancel one, the app sends that ID with your login. Anyone can send any ID.
Authentication = who are you? Authorization = are you allowed to touch this? The gym's API did the first and skipped the second. OWASP calls this Broken Object Level Authorization (BOLA); older name: IDOR, Insecure Direct Object Reference.
OWASP API1:2023: attackers "manipulating the ID of an object that is sent within the request."
03
@app.post("/bookings/{booking_id}/cancel") def cancel(booking_id: int, user=Depends(current_user)): booking = BOOKINGS[booking_id] # anyone's booking booking["status"] = "cancelled" return {"ok": True}
@app.post("/bookings/{booking_id}/cancel") def cancel(booking_id: int, user=Depends(current_user)): booking = BOOKINGS.get(booking_id) if booking is None or booking["user_id"] != user["id"]: raise HTTPException(404) # not yours = not found booking["status"] = "cancelled" return {"ok": True}
app.post("/bookings/:id/cancel", requireLogin, async (req, res) => { const result = await db.query( "UPDATE bookings SET status = 'cancelled' WHERE id = $1 AND user_id = $2", [req.params.id, req.user.id] ); if (result.rowCount === 0) return res.sendStatus(404); res.json({ ok: true }); });
Scoping the query (AND user_id = …) means a request for someone else's booking simply matches zero rows. You can't forget the check, because there is no separate check.
403 or 404? Both stop the attack. 404 also hides that booking #42 exists at all.
04
WHERE id = ? AND user_id = ?, not "fetch, then maybe check".can(user, action, object) or middleware, not a hand-written if in each route.05
Two accounts. Make an object as one, touch it as the other.
# as Alice: create a booking, note its id (say 42) curl -X POST https://api.example.com/bookings -H "Authorization: Bearer $ALICE" # as Bob: try to cancel Alice's booking curl -i -X POST https://api.example.com/bookings/42/cancel -H "Authorization: Bearer $BOB" # 403 or 404 = safe. 200 = BOLA.
def test_cannot_cancel_someone_elses_booking(client): b = client.post("/bookings", headers=alice).json() r = client.post(f"/bookings/{b['id']}/cancel", headers=bob) assert r.status_code in (403, 404)
Only test APIs you own or have written permission to test. That's the line the gym agent crossed.
06
| Bug | The question it skips | Example |
|---|---|---|
| BOLA · API1 | Can this user touch this object? | Cancel booking #42 that isn't yours |
| BOPLA · API3 | Can this user see or change this field? | Send "role": "admin" in a profile update |
| BFLA · API5 | Can this user call this function? | A normal user calls /admin/cancel-all |
07
An API for business mail tracking let anyone logged in to usps.com look up other users' account details: emails, addresses, phone numbers. Some 60 million users were exposed.
KrebsOnSecurity; Engadget, Nov 2018Its API returned any rider's age, city, weight and workout stats, even for private profiles. The first patch only required being a member, so any member could still read anyone.
Pen Test Partners via TechCrunch, May 202108
09