>_ gulhameed  /  Blog
API security · LLM platform · CVE chain

Breaking an AI Platform

I tested a self-hosted AI platform: a chat UI in front of several LLM providers, with agents that call out to real company tools through MCP. The bug that mattered most came from a chain. One verbose error told me what the platform was running. That told me which public CVE applied. And that pointed straight at the endpoint that mattered.

3 findings · Self-hosted AI platform · Chain method · ~6 min read
01 · THE SETUP

Three layers, one trust boundary

The platform is three layers stacked on top of each other.

The MCP layer is where the interesting trust boundary lives. Some servers are shared org-wide. Others are configured per-user, meaning each person authenticates individually and the token that results is supposed to be theirs alone.

I tested as one standard, authenticated account. Nothing elevated. The question that mattered was simple: does "per user" actually mean per user once that token is stored?

user 1 std acct chat backend picks model + tools LLM SDK embedded, not a proxy provider OpenAI / Gemini the LLM path · errors leak the stack (F1 / F2) MCP config /api/mcp/servers stored token shared row (bug) third-party tool helpdesk / CRM the MCP token path · per-user token leaks cross-user (F3)
Two paths, one account. The LLM path leaks the stack; the MCP token path leaks other users' credentials.
02 · THE CHAIN

Three findings, each one paying for the next

None of these needed exploit chaining in the traditional sense. What chained was information. Each finding pointed straight at the next request to send.

F1 · stack trace paths, SDK version, proj ID F2 · pinned ver line-number fingerprint advisory match version is in CVE range F3 · token leak cross-user OAuth CVSS 9.6 each finding hands the next one its map information chains, not payloads
The chain: a leaked error becomes a pinned version, becomes a matching CVE, becomes the critical.
FINDING 1

A stack trace told me the stack

The promise: internal errors stay internal.
What I did
What happened
Why it matters
POST /chat  { model: "<malformed>" }               -> full stack trace, internal paths, SDK version
POST /chat  { model: "<valid-but-unprovisioned>" }  -> upstream error, cloud project ID leaked
FINDING 2

The version in the stack trace was a known-bad version

The promise: the components behind the platform are current and patched.
What I did
What happened
Why it matters
FINDING 3
CRITICAL · CVSS 9.6

Cross-user OAuth tokens, returned to any authenticated user

The promise: a per-user credential is readable by exactly one user, the one it belongs to.
In plain terms. Onyx lets each user connect an external tool (a helpdesk, a CRM) with their own login, and that login token is meant to stay private to them. CVE-2026-71424 is the bug where asking Onyx to list the connected servers hands back another user's token: whoever connected to each tool most recently. The cause is a storage slip: each person's private token was being copied into one shared record per tool, overwriting the previous one, and every user is allowed to read that record.
What I did
What happened
GET /api/mcp/servers
-> server[3].auth_template.headers.Authorization: "Bearer •••" (user: user-a@{org}, anonymised)
-> server[5].auth_template.headers.Authorization: "Bearer •••" (user: user-b@{org}, anonymised)
how it should work A token B token A's private row B's private row only the owner reads each row what actually happened A token B token you ONE shared row last login wins any user can read the latest token per tool
The storage slip: each login overwrites one shared row per tool, which is then served to any authenticated user.
Why it matters
03 · WHERE THIS SITS

Two frameworks, same shape

This isn't a coincidence. Agentic platforms introduced a new trust boundary, per-user credentials held by a shared service, and it keeps breaking the same handful of ways across unrelated products. Recognising the shape early is half the audit.

04 · THE PATTERN

One promise breaks, the next one is free to find

None of this needed a novel technique. Each step is the system telling on itself, a little more each time, to someone who kept asking the next obvious question.

The takeaway. Don't stop at the first error message. A stack trace is a free fingerprint of every component behind the request. Pin the versions, check them against public advisories, then go test the specific thing those advisories say is broken. On a platform storing other people's credentials on your behalf, that specific thing is almost always worth checking first.

05 · REFERENCES

References

>_ gulhameed
© Gul Hameed · more writing