HighIDOR· 4 min read

The Chatbot That Handed Me Everyone Else's Conversations

A little support-chat bubble asked for a customer ID and never asked for a password — so I deleted every cookie and started reading strangers' transcripts.

Target anonymized · a retail chain's support chatbot

The target was a big regional grocery brand — the kind with weekly flyers, a loyalty program, and a website that has clearly been assembled from a dozen teams over a decade. I wasn't there for the storefront. I was there for the little chat bubble in the bottom-right corner, the one that pops up with "Hi! How can I help?" the moment you land on the help page. Support chatbots are my favorite kind of attack surface: they're almost always a separate service, bolted onto the main site late, wired up by whoever had time. Different codebase, different team, different assumptions about who's allowed to see what.

The spark

I opened the chat, typed a throwaway message, and watched Burp. Nothing exotic at first — some websocket-ish polling, some analytics noise. Then a plain POST /chatbot/api/chathistory/transcript. The response was my whole conversation, tidy JSON, timestamps and all.

The thing that made me sit up was the request body. It didn't just carry a session token doing the work quietly in a cookie. It carried a customerId — a UUID — right there in the JSON:

{"customerId":"MY_UUID","lang":"ES","metaData":{"medium":"WEB","tenant":"WEB"}}

Whenever a server returns my private data and the request explicitly names which customer's data to fetch, I get suspicious. Because now there are two possible worlds. In the good world, the server ignores that customerId for authorization and uses my session to decide what I'm allowed to see. In the bad world, the customerId is the authorization — name the customer, get the transcript. The whole bug lives in which world I'm in.

Digging in

First hypothesis: the cookie is the real gatekeeper and the customerId is just a convenience field. Easy to test. I created a second account — call it the victim — sent a message from it, and grabbed its customerId from its own chat init call. Then, from my attacker session, I sent the transcript request but swapped in the victim's customerId.

Out came the victim's conversation.

That was already a finding, but I stopped myself. This is the classic dead end that looks like a win: maybe it worked because my session was still valid and the server only checked "is this a logged-in user, yes/no?" before trusting the body. That's a weaker bug — it needs any authenticated account. I wanted to know how deep the rot went.

So I did the thing that turns a "maybe" into a "yes": I deleted every cookie on the request. No session, no auth header, nothing that said who I was. Just the body, naming a customer.

The transcript came back anyway.

An identifier sitting in a request body is not authorization. If the server hands you the data because you named the right customer, not because you proved you are that customer, you don't have an access-control bug — you have no access control at all.

That was the moment it clicked. There was no auth on this endpoint. The customerId wasn't a hint the server double-checked; it was the only key, and the lock accepted it from anyone.

The exploit

The full attack is almost boring to write down, which is exactly what makes it dangerous. Get a victim's customerId, POST it, read their chat. No login required.

POST /chatbot/api/chathistory/transcript HTTP/2
Host: app.example.com
Content-Type: application/json
Origin: https://app.example.com
Accept: */*

{"customerId":"VICTIM_UUID","lang":"ES","metaData":{"medium":"WEB","tenant":"WEB"}}

Note what's not there: no Cookie header, no Authorization. The server never asks "who are you?" — only "whose chat do you want?"

The one honest constraint is that you need the victim's customerId, and it's a UUID, so you can't just count from 1. But UUIDs leak constantly — in client-side state, in referrers, in URLs shared to support, in other endpoints of the same sprawling site. Treating a UUID as a password is the mistake underneath the mistake.

Impact

People say things to support chatbots they'd never post publicly. "Where's my order to [home address]?" "My card ending 4291 was charged twice." "I need to change the email on my account." Full names, order numbers, delivery addresses, complaints, sometimes partial payment context — all sitting behind a single guessable-if-you-have-it identifier and zero authentication. Anyone who could pair a customerId with a real person could read that person's entire support history, at scale, without ever logging in. That's a straightforward privacy breach and a tidy phishing kit rolled into one.

The fix

The endpoint must derive the customer from the authenticated session, server-side, and ignore any customerId in the body entirely — or at minimum verify it matches the session owner. Concretely: require a valid session token, resolve the caller's identity from it, and return only that identity's transcripts. Reject unauthenticated requests outright. Never let the client tell the server whose data to return. The customerId in the body should be, at most, a redundant assertion the server checks against the session — never the thing it trusts.

idorbolachatbotbroken-access-controlpiiapi-security

All identifiers, targets and payloads in this post are anonymized or defanged. Findings were reported and resolved through responsible disclosure.