Case Study: IDOR — Message Injection Into a Victim's Chat | Farchase
Farchase logoFarchase ← All Case Studies Book a Security Call
Case Studies/IDOR / BOLA
HIGHA01 · Broken Access ControlAPI1:2023 · BOLAGraphQL API

IDOR: Injecting Messages Into a Victim’s Chat

A missing object-level authorization check let any authenticated user write into — and read back — another user’s AI assistant conversation by swapping two user-controlled IDs.

CVSS v3.1
~7.6 High
Endpoint
addBrandAssistantV2Message
Status
Reported · Fix advised
Vulnerability class
IDOR / BOLA
CWE
639 · 285 · 284
Endpoint
POST /api/graphql
Root cause
Missing object-level authz

Executive Summary

An Insecure Direct Object Reference (IDOR) flaw was identified in the Brand Assistant chat feature. An authenticated user could add a message to another user's chat conversation simply by swapping the conversationId and projectId values in a GraphQL request for those belonging to a victim.

The backend authenticated the requester but never verified that they were authorized to act on the referenced conversation and project. Because both identifiers were user-controlled and trusted without an ownership check, the server executed the write against the victim's resources.

A notable secondary consequence: the mutation's response returns the target conversation's message list, so the same request that writes into the victim's chat also reads back the victim's existing messages — adding a confidentiality dimension to an integrity flaw. Rated High.

Background & Context

Each conversation is expected to be scoped to a project (projectId, belonging to a workspace) and a conversation (conversationId, belonging to a user within that project). The security contract: a user may only read or write conversations inside projects they are entitled to. The identifiers are just keys — meaningful only if the server checks that the requesting user is allowed to use them. When that check is missing, the keys become a directory an attacker can walk through. This is the defining characteristic of IDOR / BOLA.

The Vulnerability

What happened

The addBrandAssistantV2Message mutation accepts conversationId, projectId, and message from the client. When invoked, the backend:

  • Authenticates the session / auth token. ✅
  • Uses the supplied IDs to locate the target conversation. ✅
  • Never confirms the authenticated user owns or has access to that conversation and project.
  • Appends the attacker-supplied message and returns the conversation — including the victim's prior messages. ❌

The overlooked read-back

The GraphQL selection set requests messages { id role date message } on the returned conversation. Since the server processes the write against the victim's conversation, it serializes that conversation back in the response — turning a write-only IDOR into one with a data-disclosure component.

Technical Walkthrough

Documented for defensive validation. Identifiers below are placeholders/redacted.

Step 1 — Authenticate as the attacker with a valid, low-privilege account. Step 2 — Capture a legitimate write in the attacker's own chat via an HTTP proxy; note the attacker's conversationId and projectId. Step 3 — Substitute the victim's identifiers:

POST /api/graphql?on=addBrandAssistantV2Message HTTP/2
Host: <redacted>
Cookie: <ATTACKER_SESSION>
Content-Type: application/json

{
  "operationName": "addBrandAssistantV2Message",
  "variables": {
    "conversationId": "<VICTIM_CONVERSATION_ID>",
    "message": "test-injected-message",
    "projectId": <VICTIM_PROJECT_ID>
  },
  "query": "mutation addBrandAssistantV2Message($conversationId: String, $projectId: Int!, $message: String!) { addBrandAssistantV2Message(conversationId: $conversationId, projectId: $projectId, message: $message) { id userId projectId messages { id role date message } } }"
}

Step 4 — Forward. The server appends the attacker's message to the victim's conversation and returns it (with the victim's messages). Step 5 — Confirm. The message now appears in the victim's chat, confirming the missing object-level authorization check.

Root Cause Analysis

  • No object-level ownership check (BOLA). The resolver looked up the conversation by ID but never asserted the user is a member of the project or owner of the conversation.
  • User-controlled keys trusted blindly (CWE-639). Both IDs came straight from the client with no entitlement check.
  • No cross-reference validation. The server never verified the conversationId belongs to the projectId, nor that both sit inside a workspace the requester can access.

Underlying all three is the classic IDOR mistake: authentication was enforced, object-level authorization was not.

Impact Assessment

Technical impact

  • Integrity: unauthorized insertion of messages into a victim's conversation.
  • Confidentiality: read-back of the victim's existing messages via the mutation response.
  • Cross-tenant reach: the technique sweeps across other users' IDs, since nothing binds a reference to its owner.

Business impact

  • Injection of misleading or malicious content into another user's AI conversation.
  • Corruption of chat records users and workflows rely on.
  • Reputational and privacy/regulatory exposure from the message read-back.

CVSS v3.1

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L~7.6 (High)

MetricValueReasoning
Attack VectorNetworkExploited over the API.
Attack ComplexityLowA single modified request; requires knowing/guessing target IDs.
Privileges RequiredLowAny valid authenticated account.
User InteractionNoneNo victim action needed.
ConfidentialityLowVictim messages echoed back in the response.
IntegrityHighUnauthorized write into another user's conversation.
AvailabilityLowMinimal availability effect.

Scoring note: the vector computes to ~7.6 (High); the report cited 8.1, a minor calculation slip. In all interpretations the finding is High.

Remediation

Primary fix — enforce object-level authorization server-side

function addBrandAssistantV2Message(requester, projectId, conversationId, message):
    if not userHasAccessToProject(requester, projectId):
        return forbidden()
    conversation = lookupConversation(conversationId)
    if conversation is null: return notFound()
    if conversation.projectId != projectId: return forbidden()
    if conversation.userId != requester.id and not requester.canAccess(conversation):
        return forbidden()
    appendMessage(conversation, message)
    auditLog(actor=requester, action="addMessage", conversationId, projectId)
    return conversation
  • Bind every object reference to the authenticated user.
  • Validate the reference chain (user → project → conversation), not each ID in isolation.
  • Deny by default.

Defense-in-depth

  • Apply the same object-level checks to every conversation/project resolver (read, update, delete, list).
  • Use unpredictable identifiers (UUIDs) to raise enumeration cost — as hardening, not a substitute for authorization.
  • Minimize response data so a write does not return more of the target object than necessary.
  • Automated authorization tests in CI asserting user A cannot read/write user B's resources.

Lessons Learned

  • Authentication ≠ authorization.
  • Every user-controlled ID is an authorization decision.
  • Validate the whole chain, not the links.
  • Mind what the response returns — a write endpoint that echoes the target object can become a read primitive.
  • Test access control negatively: "Can user A act on user B's object?"

Summary

Type
IDOR / Broken Object-Level Authorization
Endpoint
addBrandAssistantV2Message
Vulnerable params
conversationId, projectId
Root cause
Missing object-level ownership checks
Severity
High · CVSS ~7.6
Fix
Enforce ownership chain server-side; deny by default

Do Your APIs Check Object Ownership?

IDOR/BOLA is the #1 API risk — and invisible to scanners. Farchase tests every object reference against real user permissions.