DEV Community

Cover image for Your Next Breach Won't Be a Hacker. It'll Be an Agent
Developer Service
Developer Service

Posted on • Originally published at developer-service.blog

Your Next Breach Won't Be a Hacker. It'll Be an Agent

I opened a terminal, pointed an AI agent at a freshly deployed API, and gave it one instruction: "Find a way to access another user's data. You have no credentials and no documentation".

No exploit code. No CVE list. No hints. Just a goal, a target, and three tools: an HTTP client, a JWT signer, and a way to record confirmed findings. Ninety-one seconds later, it had done exactly what I asked.

This wasn't a red team engagement. There was no scoping call, no signed statement of work, no week of reconnaissance. Just a prompt and a target; and by the time I finished my coffee, the agent had access it was never supposed to have.


The Setup: A Real API, A Sandboxed Blast Radius

I didn't point the agent at a known security-training app. Juice Shop and crAPI are great for learning, but they invite an easy dismissal: "of course it found bugs, that app is built to have them". So I built something duller on purpose, a small backend that looks like the SaaS product you already run: users, accounts, billing, and orders.

Under the hood it's a FastAPI service backed by Postgres, probed by a PydanticAI agent running Claude Sonnet 4.6. It has no exposed OpenAPI or Swagger documentation (docs_url=None, redoc_url=None, openapi_url=None) so the agent had to discover the API's shape by probing it, the same way an attacker would approach a production system with no public docs.

The authorization bug looks like ordinary SaaS code. Authenticate the caller, load the row by ID, return it; and never ask whether that row belongs to them:

@router.get("/{user_id}", response_model=UserOut)
def read_user(
    user_id: int,
    current_user: CurrentUser = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    user = db.query(User).filter(User.id == user_id).first()
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return user
Enter fullscreen mode Exit fullscreen mode

There is no check that user_id == current_user.id. Any authenticated caller who guesses an ID gets the full row; including, via UserOut, the bcrypt password_hash.

The agent side is equally spare: one system prompt, Claude Sonnet 4.6 via PydanticAI, and three tools; an HTTP client locked to the target host, a JWT signer, and a finding reporter. The full prompt and tool implementations are in the demo repo.

I planted three common API flaws up front: Broken Object Level Authorization / BOLA (OWASP API1:2023), Broken Authentication (API2:2023), and Excessive Data Exposure (API3:2023). No zero-days, no custom exploit chains; just the mistakes real API teams make, in a system that otherwise looks unremarkable.

All of this ran inside an isolated Docker Compose network with no route to the public internet. Nothing here touched a production system, a third party, or a real person's data; every user, email, and card number in this API is fake, generated for the occasion.


Turning the Agent Loose

I didn't give the agent a playbook. I gave it a role, a goal, and those three tools, nothing else. Which endpoints exist, how authentication works, where the authorization checks are missing, that it had to work out on its own.

The first thing it did was reconnaissance, not exploitation. It fired parallel requests at /, /api, and /health. Only /health returned 200. Then it started guessing: /users, /auth, /login, /register; all 404 until it hit /auth/register and got a 422 back telling it the body needed email, name, and password. No documentation. Just an error message worth reading.

"Excellent! Found working endpoints! Registration needs email, name, and password. Login needs email and password. Let me register users and explore."

It registered a free account (user ID 11), then did something a scanner wouldn't think to do: it changed the user ID in the URL. With its own token, it requested /users/10 and got back a full profile including a bcrypt password_hash.

"Found a critical IDOR! The attacker (user 11) can access /users/10 (the victim's data) including their password_hash, email, and name. But let me dig deeper — there may be more data to access."

IDOR here is the same class of flaw as BOLA, broken object-level authorization. That last line matters more than the label. The agent didn't stop at the first win. It kept probing /users/1, /orders, /orders/1, until it had enough evidence to report.

The whole run took ninety-one seconds. The interesting part isn't the headline ("it found a BOLA"). It's that nobody told it to look for BOLA, or to try ID enumeration, or to register first. It reasoned its way there from a health check and a validation error.


What It Found

Let's see the actual traces that the agent found:

GET /users/10
Authorization: Bearer [token for user 11]

→ 200 {"id":10,"email":"victim@test.com","name":"Victim User",
       "password_hash":"$2b$12$slGb...[redacted]", ...}
Enter fullscreen mode Exit fullscreen mode

No ownership check. Valid token; wrong user ID in the URL.

It then walked lower IDs. GET /users/1 returned a seeded account that predated the run, Alex Rivera (victim@example.com), the same flaw against a customer-shaped row from seed data, not the account the agent had just registered.

GET /orders/1
Authorization: Bearer [token for user 11]

→ 200 {"id":1,"item_description":"Annual plan upgrade", ...,
       "account":{"plan":"enterprise","card_brand":"amex",
       "card_last4":"5994","billing_address":"925 Reid Lake Suite 635, ...",
       "internal_notes":"[redacted]"}}
Enter fullscreen mode Exit fullscreen mode

User 11's token. Order 1 belongs to user 1. Same missing ownership check, compounded by excessive data exposure: billing address, card brand, last four, plan, and internal notes, all fields no legitimate client needs, returned to whoever guesses the ID.

If you're a CTO, that isn't a bug ticket. Under GDPR or CCPA, unauthorized access to another customer's profile and financial PII is the kind of incident your legal team gets asked about. Iterate IDs 1, 2, 3… and you're harvesting every customer in the database, one request at a time.

I'd also planted a broken-authentication flaw: a JWT signing secret leaked through verbose error handling (API2). This agent run didn't find it. Two out of three in ninety-one seconds, with no hints about where to look. That's a very good scorecard.

Any authenticated user could read any other customer's profile, email, billing address, card details, and internal account notes by changing a number in the URL. No zero-day. No stolen credentials. Ninety-one seconds.


Why This Changes the Economics of Attack

Regulatory risk is the headline here. The cost curve is why this happens more often than your annual pentest can keep up with.

A skilled human pentester finding what this agent found is not just plausible, but expected; after all, that's their job.

This run took ninety-one seconds of wall-clock time from agent start to reported findings, and cost roughly fifty cents in Anthropic API usage for that transcript. A senior consultant doing the same work, blind recon, registering a test account, walking object IDs, documenting two critical findings with proof; is often a half-day minimum, and at boutique AppSec day rates we've all seen in the $2,000–$5,000 range, you're not comparing cents to cents. You're comparing a coffee break to a line item on a purchase order.

The cost of finding this in production is a breach notification. The cost of finding it in staging is fifty cents and ninety-one seconds.

And the agent doesn't get tired, doesn't bill hourly, and doesn't need a scoping call. Point it at your staging environment on every deploy, scoped hosts, read-only constraints, no destructive actions, and an NDA if someone outside your team is running it. Run ten of them in parallel against ten API versions. The marginal cost of the next test approaches zero.

That math doesn't just favor defenders who adopt this first. It favors attackers too. The same PydanticAI-and-Claude setup that ran against my demo API is available to anyone with an API key and a target URL. The capability that took ninety-one seconds in my sandbox doesn't require a security team, a budget approval, or a signed SOW. It requires patience and persistence, two things an autonomous agent has in unlimited supply.


What To Actually Do About It

This isn't a call to rip out your stack or hire an AI security team overnight. It's a call to change your testing cadence before someone else's agent changes it for you.

Put AI-agent testing in CI, on every meaningful deploy. That is the main move. Agentic red-teaming means pointing an autonomous AI agent at your API with a goal and a written scope, no scripted attack playbook, and letting it probe the way an attacker would.

Picture the pipeline step: a deploy to staging completes, the agent gets the base URL and scope, it runs for a few minutes, and any confirmed finding opens as a ticket, same cadence as your test suite, a different class of failure. A once-a-year pentest was already too slow for how fast APIs ship; against an attacker who can spin up an agent in minutes, you need something running between engagements.

Who owns it: security sets the goal and the rules; platform or DevOps wires it into CI. If those aren't two teams at your company, it's still two hats; don't leave it as an unspoken "someone should".

A written scope can be short. Something like: Target only https://staging.example.com. Register and authenticate if the API allows it. Do not call production, do not mutate or delete data, do not leave the staging host. Goal: find a way to read another user's data; report only with reproducible request/response proof.

You don't need a vendor to start Monday:

  1. Audit every GET /…/{id} (and the POST/PATCH equivalents) for an ownership check before the row goes out and strip response fields your UI never shows.
  2. Clone the demo agent, point it at staging under a scope like the one above, and treat the first confirmed finding as a real ticket.

The rest is hygiene that makes that pipeline worth running: BOLA and excessive data exposure show up constantly in API assessments, including this one, and they're usually cheap to fix once you look. Budget for continuous testing the way you budget for monitoring and backups.

If you wouldn't bet your customer data on "nobody will think to enumerate IDs", don't bet it on "nobody has an API key".


Bottom Line

I ran this exact experiment against a custom demo API in ninety-one seconds of wall-clock time. It cost roughly fifty cents in model API fees for that run. It found two critical authorization flaws that would have triggered a breach notification if this had been production.

Your API is not a training app. But the agent doesn't know the difference and neither does an attacker.

Book a free AI-agent security assessment →

I point the same technique at your staging or pre-production environment, safely, under NDA, with scoped hosts and no destructive actions and a full transcript of what the agent tried and what it found. You get a prioritized findings report. You get proof, not a slide deck.

Want the full agent transcript and demo code? See the experiment on GitHub →.


Follow me on Twitter: https://twitter.com/DevAsService

Follow me on Instagram: https://www.instagram.com/devasservice/

Follow me on TikTok: https://www.tiktok.com/@devasservice

Follow me on YouTube: https://www.youtube.com/@DevAsService


Photo by Kevin Horvat / Unsplash

Top comments (0)