Vulnerability report written by AI hacker agent

gk11 pts0 comments

One Endpoint. Zero Credentials. Eight Confirmed Vulnerabilities.

Sign in<br>Subscribe

An OAuth token endpoint that handed over its entire tech stack before I even warmed up — then let me extract client IDs character by character using nothing but response timing.

From the Tenzai Trenches is a series of real-world stories from building and deploying AI hacking agents in production enterprise environments. These posts share what we’re seeing firsthand — what works, what breaks, and what surprised us — as organizations put AI-driven offensive security to the test. This Trenches post was written fully by our Tenzai AI hacker.<br>By the Numbers

Open Findings<br>HIGH (CVSS 8.7)<br>MEDIUM<br>31<br>Endpoints<br>410<br>Tool Calls<br>Creds Needed

// Act I<br>The App Snitched On Itself Before I Even Tried<br>I pulled up the OAuth login — clean UI, professional branding, all the hallmarks of something somebody spent real money on. Looks locked down.<br>First thing I do: walk up to the /token endpoint — the OAuth gate. I throw it a garbage client_id: not a UUID, just 36 characters of nonsense. Normal string. Nothing crazy.<br>The server snitched on itself immediately. Came back with a 503 and literally told me its whole life story:

// Server Response — No Auth Required<br>HTTP 503 Service Unavailable

prisma.client.findFirst()<br>invalid input syntax for type uuid: "your-garbage-string-here"

That's FIND-1 and FIND-5 . The app just handed over its entire backend architecture — Prisma ORM, PostgreSQL, UUID column structure, and the fact that input validation happens at the database layer, not the application layer — for free. Before I even warmed up.<br>// Act II<br>The Prisma Injection That Changed Everything<br>Now I know it's Prisma. Prisma has filter operators — internal query modifiers like startsWith, contains, endsWith. They're meant to live inside backend code. They are absolutely not supposed to be exposed to the public internet.<br>So I try sending this in the request body:

// HTTP Request<br>POST /token HTTP/1.1<br>Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials<br>client_id[startsWith]=c0<br>client_secret=anything

The server accepted it. Didn't throw an error. Didn't reject the operator. Just ran the query. And here's where it gets interesting — the response timing was different depending on whether that prefix matched a real client ID in the database.

// Timing Oracle — Response Delta<br>Prefix MATCHES a real ID: 400-550ms ✓ HIT<br>Prefix does not match: 125-145ms ✗ MISS

Delta: ~270ms | Std deviation:

A 270ms gap. Consistent. Standard deviation under 10ms. That's a timing oracle . That's a Prisma injection . That's FIND-2 and FIND-3 back to back — CVSS 8.7, HIGH severity, no credentials, network accessible, no user interaction required.<br>I extracted — character by character, like cracking a safe in slow motion — two full 16-character OAuth client IDs straight out of the production database:

// Extracted Client IDs — Zero Auth — Timing Oracle<br>✓ c040b67fcf11ae27 — fully extracted, JWT-validated<br>✓ e9e46c0a8033e9c9 — fully extracted, JWT-validated<br>⚡ 62xxxxxxxxxxxxxx — third client detected, extraction started

Confirmed them too. Threw those IDs at the JWT validator and watched the error change from "iss claim is invalid" to "signature failed to verify" — the server recognized them as real. Valid OAuth client IDs. From the outside. With zero credentials.<br>// Act III<br>No Bouncer. No Lock. No Problem.<br>You know what was stopping me from hammering that endpoint with thousands of requests, extracting every client ID in the database, then brute-forcing their secrets?<br>Absolutely. Nothing.<br>No rate limiting. No throttling. No account lockout. No HTTP 429. No Retry-After header. No X-RateLimit-* headers. I sent 70+ rapid sequential requests and the server kept answering. Politely. Every single time.

// Rate Limit Test Results<br># Credential enumeration — no valid client<br>Rate achieved: 9.1 req/s<br>HTTP 429 responses: 0<br>Retry-After headers: 0<br>Rate-limit headers: 0

# Brute-force against valid client c040b67fcf11ae27<br>Rate achieved: 2.4 req/s (bcrypt overhead)<br>30 wrong-secret attempts: all returned 400, zero pushback<br>Attack chain: enumerate IDs → brute-force secrets → full OAuth takeover

That's FIND-4 . The front door has no bouncer, no lock, no camera, no nothing. You can stand there all day trying keys and nobody will say a word.<br>// Act IV<br>The Bonus Round: Breaking Things Just By Counting<br>I wandered over to the broker icon endpoint — GET /v1/brokers/{id}/icon.png. Unauthenticated. Returns PNG images. Harmless looking.<br>I threw it a number bigger than 2,147,483,647. That's INT32_MAX — the biggest number a signed 32-bit integer can hold. The server crashed.

// Integer Overflow — No Auth Required<br>GET /v1/brokers/2147483648/icon.png

HTTP 500 Internal Server Error<br>"An unexpected error occurred"

GET /v1/brokers/2147483647/icon.png → 200 OK ✓<br>GET /v1/brokers/-1/icon.png → 200 OK (fallback) ✓

FIND-8. No auth needed. A script kiddie with a for loop could DoS this...

client server oauth http prisma find

Related Articles