A GraphQL Authorization Bypass in BrowserOS Exposed Private AI Conversations
#On this page
While testing the GraphQL API behind BrowserOS, an open-source AI browser, I captured the request it used to load conversation messages. The query included a userId filter. I removed it and replayed the request with the same account and session.
The response included private messages from other BrowserOS users.
There was no token manipulation, privilege escalation, or identifier guessing. I changed only the GraphQL body of an ordinary authenticated request.
The query that crossed the tenant boundary
With the client-supplied userId filter removed, the query was:
query ConversationMessages($pageSize: Int!) {
conversationMessages(first: $pageSize) {
nodes {
rowId
conversationId
message
}
}
}The server returned 200 OK, followed by message records from conversations outside my account:
{
"data": {
"conversationMessages": {
"nodes": [
{
"rowId": "[REDACTED]",
"conversationId": "[REDACTED]",
"message": {
"role": "user",
"parts": [
{
"text": "[REDACTED: message belonging to another user]",
"type": "text"
}
]
}
}
]
}
}
}The returned conversationId values did not belong to any conversation in my account. My cookies and authentication context were unchanged. Removing the filter was the entire exploit.
The root collection, once the userId filter disappeared:

The filter was not authorization
BrowserOS authenticated the request correctly, but treated the userId filter as optional client input instead of deriving ownership from the authenticated session.
Anything in a GraphQL request can be changed before it reaches the server. The normal client was well behaved. An attacker did not have to be.
Direct object queries appeared to enforce ownership or fail safely. The root collection was the gap: remove its filter, and it returned rows across users.
Why pagination made this more than a single-record IDOR
The collection supported cursor pagination through arguments such as first and after. This was not a one-record IDOR that required guessing identifiers: the resolver supplied the messages and their conversation IDs, page by page.
In practice, the query exposed:
- Full user-authored message content
- Conversation identifiers
- Internal message row identifiers
AI conversations are particularly sensitive data. People paste credentials, code, internal documents, customer details, personal questions, and half-formed thoughts into assistants precisely because the interface feels private.
The vulnerability turned that private context into a collection another authenticated user could enumerate.
The schema suggested a wider authorization review
After confirming the message exposure, I inspected the GraphQL schema through introspection. Five other generated root collections deserved review for similar tenant-scoping issues:
| Collection | Potentially sensitive fields |
|---|---|
profiles | Names, avatar URL, user ID, preferences, company, and role |
conversations | Profile IDs and conversation metadata |
scheduledJobs | User-authored prompts, names, schedules, and profile IDs |
scheduledJobRuns | Results, execution logs, browsing activity, and tool calls |
llmProviders | Provider configuration and per-user infrastructure details |
I confirmed cross-user access only on conversationMessages. I included the other collections in my report as schema findings for the team to review, not as demonstrated vulnerabilities.
The fix happened behind the API
I reported the issue privately to BrowserOS on April 13, 2026, with the request, a redacted response, impact analysis, and remediation guidance.
On June 3, a BrowserOS founder told me over Discord that the issue had been fixed in the backend.
Because this was a server-side change and I was not given its implementation details, I cannot point to a public patch or say whether the fix was implemented in the resolver, through database row-level security, or another way.
What I would carry into the next review
- Client-supplied filters can shape a query, but authorization must be enforced on the server.
- Test collection queries even when direct lookup by ID is protected.
- Treat pagination as an impact multiplier when a collection is unscoped.
- Use two accounts in authorization tests and audit sibling resolvers when one is missing tenant enforcement.
Disclosure timeline
- April 13, 2026: Privately reported the cross-user message exposure with a working GraphQL proof of concept and redacted response data
- June 3, 2026: A BrowserOS founder confirmed by Discord DM that the issue had been fixed in the backend
- July 1, 2026: Public disclosure