0%~/soraf
↓ contact
← index

cat case/001-the-wildcard.md

Case 001 / 2026

How a wildcard subscription read ten strangers’ conversations in three minutes.

One character in an MQTT topic string separated my session from everyone else’s. This is the full disclosure, including the part where triage said no.

Target
Live support chatbot
Program
Private HackerOne
Severity
Critical
Outcome
First 4-digit bounty

This is the story of a critical bug in a live customer support chatbot on a major website — and the first four-digit bounty I ever earned. No zero-day. No exploit chain. The evidence was already public.

01 — The target

I was hunting on a private HackerOne program. The main site looked ordinary. In the corner sat the familiar support chat bubble — the one you click when you want help.

I did not start with scanners. I did the thing that still works: I viewed the page source. That is the raw HTML and embedded configuration your browser downloads before it draws anything. Companies leave API URLs, feature flags and backend hostnames in there more often than they would like to admit.

02 — A chat API URL hidden in the page

Inside the source was a Drupal settings JSON block — a bag of configuration the front end needs. I searched it for anything chat-related. One object held the integration, and one field mattered:

apiUrl: "https://chat-backend.[REDACTED]:8081/custom?token=[redacted]"

The homepage was telling every visitor where the chat application actually lives. Not on www.[REDACTED].com — on a separate host, on port 8081. If something was misconfigured, it would be there.

03 — env_app.js, served to anyone

I moved recon to that host and started collecting JavaScript. Among the files was env_app.js — environment configuration for the chat application. No login. No cookie check. It simply loaded.

CWC_CONNECTION_USERNAME: 'cwc_user'
CWC_CONNECTION_PASSWORD: '[redacted]'
REACT_APP_CWC_MQTT_URL: 'wss://chat-backend.[REDACTED]:8081/mqtt'

Broker URL and credentials, in a public file. Even stopping there would have been a finding: it tells an attacker exactly where to knock and which keys might work.

But I did not know what MQTT was. I had never used it in a hunt. I only knew the chat felt real-time, and this configuration was how it got there.

04 — What MQTT actually means

Imagine a post office in the middle. Apps do not message each other directly; they send packets to a broker. Each packet goes to a topic, which is a channel name. Other apps subscribe to the topics they care about.

For this system, each customer session had its own topic shape:

client/{session-id}/chat_session

That is good design — if the broker only lets you subscribe to your session. But MQTT supports wildcards. The + symbol means match any single level here:

client/+/chat_session

That means every customer’s chat at once. If the broker accepts it, you are no longer listening to one conversation — you are listening to the whole support queue.

subscribe10 sessions
  • client/7f3a91/chat_sessionexposed
  • client/b2e0c4/chat_sessionexposed
  • client/19d8ae/chat_sessionexposed
  • client/e6402f/chat_sessionexposed
  • client/a91c73/chat_sessionyours
  • client/3c5b18/chat_sessionexposed
  • client/d07f62/chat_sessionexposed
  • client/58a4eb/chat_sessionexposed
  • client/c13d80/chat_sessionexposed
  • client/6e2f45/chat_sessionexposed
One character. Ten live customer sessions.Click the topic to narrow it.

05 — The fallback in main.js

The credentials in env_app.js were for cwc_user, not an administrator. So I followed the front-end build. I opened asset-manifest.json, used it to locate main.js, and read the minified bundle looking for needles.

There was a fallback in the logic:

username: "admin"
password: "admin"

Developers leave a default account for local testing and forget it on the server. The code said admin / admin. When I tested the broker, the live misconfiguration was weaker still: admin with an empty password worked.

06 — Building the test

I used AI to learn the client commands — brokers, topics, subscribe versus publish — and turned it into a small Node proof of concept with the mqtt library.

npm install mqtt

node -e "const c=require('mqtt').connect(
  'wss://chat-backend.[REDACTED]:8081/mqtt',
  {username:'admin',password:'',rejectUnauthorized:false}
);
c.on('connect',()=>{
  console.log('CONNECTED');
  c.subscribe('client/+/chat_session',{qos:0},()=>console.log('SUBSCRIBED'))
});
c.on('message',(t,m)=>console.log(t+': '+m.toString()));
setTimeout(()=>process.exit(),60000)"

No company account. No VPN. No special network. Just Node and network access to a broker URL that was already in public JavaScript.

07 — When it worked

It connected. It subscribed. Then lines started appearing, fast. This was not a staging sandbox — these were live customer support chats happening at that moment. Each line was a topic and a JSON payload: session metadata, chat history, sometimes JWT tokens and session IDs.

When customers typed personal details into the chat — phone numbers, home addresses, service information — those appeared in the same stream. Some messages were security-sensitive in their own right, such as SIM unlock requests.

I let it run for about three minutes. In that window I counted 88 messages across 10 different customer sessions.

Then I stopped.

08 — Why this breaks trust

Two failures stacked:

  • Authentication. The broker accepted a trivial admin login with no password.
  • Authorization. That user could subscribe to a wildcard topic and receive many unrelated sessions.

An attacker could sit anywhere in the world and read active support chats in real time, without registering an account. No complex chain — one script, built from clues already public in JavaScript files.

09 — Reporting and outcome

I packaged the finding in mid-March 2026: reproduction steps, the Node command, a short PoC video, redacted samples, and a plain impact statement.

Triage did not end in one message. I had to explain the real issue more than once. In the end the program accepted it as a valid critical-severity issue — and for me it was also personal: my first four-digit bounty.

10 — The triage call

The first reply sounded like a duplicate of older reports. The program had already reviewed similar findings, where exposed chat configuration and a public MQTT connection were judged intentional — the widget is public, and authentication happens later. In that earlier review, you could not read other people’s messages.

I replied calmly and stuck to facts. A public broker connection might be acceptable by design. That was not the core bug. The core bug was authorization on MQTT topics. My PoC video showed real fields — home addresses, phone numbers, service IDs — flowing in real time. I asked one direct question: did the older reports demonstrate live cross-customer data, or only exposed config files? Those are not the same severity.

That message changed the tone. The engineering team retested and confirmed:

“We tested this and confirmed on the engineering side that it was real customer data. Earlier reports did not show any live data leaks. The chatbot has been taken down until the issue is fixed.”

A few days later they asked me to retest the fix and share IP addresses and timestamps so they could improve logging. I retested: the wildcard subscription no longer worked. The chatbot stayed offline until the backend was safe.

11 — Closing

Boring recon still wins. Page source led to the chat apiUrl; the chat host led to env_app.js; that file led to a protocol I had to learn on the fly; and the application bundle gave the last hint about which account to try.

AI helped me learn and assemble the test. The evidence came from what was already exposed on the public internet.

Originally published on Medium, 3 June 2026. Target details redacted. No customer data retained.

Thanks to @emptymahbob.