A GraphQL Authorization Bypass in BrowserOS Exposed Private AI Conversations

·4 min read

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.graphql
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:

response.json
{
  "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:

Gary Oldman shouting “Everyone!”

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:

CollectionPotentially sensitive fields
profilesNames, avatar URL, user ID, preferences, company, and role
conversationsProfile IDs and conversation metadata
scheduledJobsUser-authored prompts, names, schedules, and profile IDs
scheduledJobRunsResults, execution logs, browsing activity, and tool calls
llmProvidersProvider 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