August 27, 2026
One Restaurant Account. 23,000+ Order IDs. One Very Big MQTT Problem
While testing a partner-facing live order dashboard, I found that a valid low-privilege restaurant account could connect to the…
By Mohammed Ashraf
8 min read
While testing a partner-facing live order dashboard, I found that a valid low-privilege restaurant account could connect to the application's MQTT infrastructure and subscribe to wildcard topics such as #.
Five minutes later, I had received 8,288 live messages associated with 4,114 restaurants and 409 drivers across 11 countries.
A longer 60-minute capture produced 147,817 messages, covering 23,929 unique (tenant, restaurant) combinations, roughly 23,000 unique OrderIds, 3,777 drivers, and activity from 12 countries.
And then I found a second issue.
The live MQTT stream exposed fresh OrderIds belonging to other restaurants, while a separate order-items endpoint accepted those OrderIds without authentication.
So the final chain was essentially:
Cross-tenant MQTT access
↓
Live OrderId disclosure
↓
Unauthenticated order lookup
↓
Live order contentsCross-tenant MQTT access
↓
Live OrderId disclosure
↓
Unauthenticated order lookup
↓
Live order contentsI started by looking at some JavaScript.
I really should have gone to bed.
It Started With Way Too Much JavaScript
I was testing a partner-facing web application and doing the usual frontend reconnaissance.
Download JavaScript.
Search for interesting strings.
Find 25 MB of something that looks like it was bundled by a computer having a bad day.
Repeat.
Eventually, I found a large live-order application bundle and its source map.
I started searching for the usual keywords:
secrets
iot
mqttsecrets
iot
mqttThat led me to references to an AWS IoT configuration endpoint used by the live-order application.
The frontend was retrieving information required to establish an MQTT connection.
That immediately raised a question:
If the browser can obtain the IoT configuration, how tightly is the resulting MQTT access actually scoped?
Time to find out.
The Frontend Was Getting MQTT Credentials
While watching the browser's network requests, I noticed that loading the live-order dashboard triggered a request to retrieve IoT configuration.
The request required a legitimate partner access token and some application-specific headers.
The response contained the information necessary for the frontend to establish an MQTT connection, including the IoT endpoint and authentication material.
The authentication context also contained restaurant and tenant information.
Conceptually, it looked like:
{
"RestaurantId": "[REDACTED]",
"Tenant": "[REDACTED]",
"ClientId": "[REDACTED]"
}{
"RestaurantId": "[REDACTED]",
"Tenant": "[REDACTED]",
"ClientId": "[REDACTED]"
}So the application clearly knew which restaurant the authenticated user belonged to.
That was good.
Now I wanted to know whether the MQTT authorization layer knew it too.
What Does a Normal Restaurant Actually Need?
I traced the MQTT subscriptions made by the legitimate application.
The frontend appeared to subscribe to only a small number of application-specific topics.
Conceptually:
partners/{tenant}/holidaysurveys
partners/{tenant}-{ref}/holidaysurveys
liveorders/{ref}/debugpartners/{tenant}/holidaysurveys
partners/{tenant}-{ref}/holidaysurveys
liveorders/{ref}/debugThat's it.
Nothing resembling:
##Which made sense.
A restaurant shouldn't need access to every event generated by every other restaurant.
Then came the obvious question:
What happens if I ask for # anyway?
The "Oh No" Moment
I connected using a legitimate partner token and made a completely passive subscription test.
First:
client.subscribe("#", qos=0)client.subscribe("#", qos=0)The broker returned:
SUBACK 0x00SUBACK 0x00Okay.
That was unexpected.
So I tried another wildcard:
client.subscribe("partners/+#", qos=0)client.subscribe("partners/+#", qos=0)Again:
SUBACK 0x00SUBACK 0x00Then:
client.subscribe("+/+", qos=0)client.subscribe("+/+", qos=0)Same result.
At this point I had the classic security-researcher experience:
- Try something because it probably won't work.
- It works.
- Stare at the terminal.
- Try it again because surely you typed something wrong.
- It works again.
- Coffee becomes mandatory.
The authenticated restaurant account could apparently request wildcard MQTT subscriptions far outside the topics used by the legitimate application.
60 Seconds Later…
I left the connection running for about a minute.
The result:
351 messages
177 unique OrderIds
177 unique (tenant, restaurant) combinations
6+ countries
~40 drivers351 messages
177 unique OrderIds
177 unique (tenant, restaurant) combinations
6+ countries
~40 driversMaybe it was some internal test traffic.
Maybe I had just caught an unusually busy minute.
So I extended the capture.
Five minutes:
8,288 messages
4,114 unique (tenant, restaurant) combinations
~4,000 unique OrderIds
11 countries
409 drivers8,288 messages
4,114 unique (tenant, restaurant) combinations
~4,000 unique OrderIds
11 countries
409 driversAt this point I was no longer asking:
"Is this broken?"
I was asking:
"How broken is this?"
The Longer Capture
Short captures can sometimes be misleading.
So I ran a much longer passive capture and analyzed the resulting traffic.
The 60-minute capture contained:
147,817 messages
23,929 unique (tenant, restaurant) combinations
~23,000 unique OrderIds
12 countries
3,777 unique drivers147,817 messages
23,929 unique (tenant, restaurant) combinations
~23,000 unique OrderIds
12 countries
3,777 unique driversI also ran another 90-minute capture during separate testing:
22,905 messages
2,631 unique (tenant, restaurant) combinations
2,625 unique OrderIds
11 countries
200 drivers22,905 messages
2,631 unique (tenant, restaurant) combinations
2,625 unique OrderIds
11 countries
200 driversThe exact numbers obviously depend on the time of day and production traffic.
But the important observation was consistent:
This wasn't one restaurant accidentally seeing one other restaurant.
The subscription was exposing activity across a large shared production environment.
What Was Actually In The Messages?
This is where I wanted to be very careful.
It would be easy to write:
"The MQTT bus contained everyone's personal information!"
It didn't.
I scanned approximately 147,000 payloads and found no evidence of several categories of customer PII I specifically searched for.
I found:
- No customer phone numbers
- No customer email addresses
- No customer delivery addresses
- No customer GPS coordinates
- No payment information
- No authentication tokens
So no, this wasn't a situation where someone's home address was casually flying through MQTT.
The actual exposed information was different.
And still problematic.
The Event Types
Across the large capture, I observed only eight recurring event types:
orderchangestate
driverassigned
driveratrestaurantestimated
restaurantinfoupdatedevent
restaurantmenuupdatedevent
startfoodprep
eposfailedtosendtokitchen
HolidaySurveyorderchangestate
driverassigned
driveratrestaurantestimated
restaurantinfoupdatedevent
restaurantmenuupdatedevent
startfoodprep
eposfailedtosendtokitchen
HolidaySurveyThe majority of the traffic consisted of order-state events.
Driver-related events contained information such as:
DriverName
DriverId
EstimatedArrivalTimeDriverName
DriverId
EstimatedArrivalTimeOther events exposed tenant, restaurant, and order identifiers.
One important correction from my initial notes:
An eposfailedtosendtokitchen event did not contain order-item details.
It contained identifiers such as:
Tenant
RestaurantId
OrderIdTenant
RestaurantId
OrderIdThat's it.
I went back through the logs specifically to verify this because an earlier interpretation had suggested additional order information.
The logs disagreed.
The logs win.
The Data Inventory
Here's the actual picture from the 60-minute capture:
FieldObservationImpactDriverName3,777 unique valuesDriver identificationDriverId~99.9% emptyVery limited standalone valueEstimatedArrivalTimePresent on driver eventsReal-time activity trackingTenantPresent throughout the streamTenant enumerationRestaurantIdPresent throughout the streamRestaurant identificationOrderIdPresent in relevant eventsChain keyLegacyOrderIdPresent in ~34% of messagesCross-system correlation
There were also 50,658 distinct LegacyOrderId values observed in the 60-minute capture.
These are short numeric identifiers that can link the newer order identifiers to legacy systems.
Again, the interesting part isn't that a single identifier existed.
It's that the identifiers were being exposed across tenant boundaries.
The Country Spread
The traffic wasn't limited to one local market.
I observed activity associated with 12 countries during the longest capture.
Several countries appeared consistently across the captures, while one market appeared more isolated.
I'm intentionally not publishing the actual country-to-tenant mapping or production identifiers here.
It's unnecessary for understanding the vulnerability.
The important observation is simply:
The MQTT namespace was shared across multiple geographic markets, and the wildcard subscription exposed cross-market traffic.
But Driver Names Alone Aren't P1
At this point, I had a pretty clear cross-tenant authorization issue.
But let's be honest.
If the impact were only:
"You can see some restaurant events and driver names."
that's serious, but the severity discussion would be much more limited.
Then I remembered another endpoint.
And this is where the story gets significantly worse.
The Second Vulnerability
During earlier testing, I had identified an order-items API with functionality conceptually similar to:
GET /applications/assistantapi/v1/order-item?orderId={OrderId}GET /applications/assistantapi/v1/order-item?orderId={OrderId}The endpoint did not require authentication when accessed from the server side.
On its own, an unauthenticated order lookup is obviously interesting.
But there was a problem:
Where do I get valid OrderIds?
Then I looked back at the MQTT stream.
The answer was sitting there.
Everywhere.
MQTT → Order API
The MQTT stream continuously exposed fresh alphanumeric OrderIds belonging to restaurants outside the authenticated account's scope.
I took a small number of freshly observed identifiers and tested them against the order-items endpoint.
I deliberately kept the validation small.
I didn't need thousands of orders.
I didn't need a database.
I didn't need to download everything the broker would give me.
A handful of fresh identifiers was enough to prove the chain.
The result:
8 / 8 alphanumeric OrderIds
→ HTTP 2008 / 8 alphanumeric OrderIds
→ HTTP 200The response contained actual order information, including fields such as:
items
item names
unit prices
options/sub-items
total price
order numberitems
item names
unit prices
options/sub-items
total price
order numberA sanitized example looked like:
{
"items": [
{
"name": "[REDACTED]",
"unitPrice": 12.90,
"subItems": [...]
}
],
"totalPrice": "[REDACTED]",
"OrderNumber": "[REDACTED]"
}{
"items": [
{
"name": "[REDACTED]",
"unitPrice": 12.90,
"subItems": [...]
}
],
"totalPrice": "[REDACTED]",
"OrderNumber": "[REDACTED]"
}All real production identifiers and values have been removed from this article.
What About Legacy IDs?
I also checked whether the short numeric LegacyOrderId values could be used directly with the same endpoint.
They couldn't.
The numeric identifiers returned:
404 No order found404 No order foundThe alphanumeric OrderIds obtained from the MQTT stream were the ones that successfully resolved.
That distinction is important because it demonstrates that the MQTT stream wasn't merely leaking irrelevant identifiers.
It was leaking the exact identifier format needed for the second vulnerability.
The Full Attack Chain
At this point the complete chain looked like this:
┌─────────────────────────────┐
│ Valid low-privilege account │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Retrieve legitimate MQTT │
│ connection configuration │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Subscribe to wildcard topic │
│ # │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Receive live OrderIds from │
│ unrelated restaurants │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Unauthenticated order-items │
│ API │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Retrieve order contents │
└─────────────────────────────┘┌─────────────────────────────┐
│ Valid low-privilege account │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Retrieve legitimate MQTT │
│ connection configuration │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Subscribe to wildcard topic │
│ # │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Receive live OrderIds from │
│ unrelated restaurants │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Unauthenticated order-items │
│ API │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Retrieve order contents │
└─────────────────────────────┘Or, in the shortest possible version:
MQTT = feeder
API = dataMQTT = feeder
API = dataThat's the part that made the severity jump.
The Real Authorization Problem
The legitimate application appeared to require access to only a handful of MQTT topics.
Yet the broker accepted:
##for a normal authenticated partner account.
That's the fundamental problem.
The authentication system knew the identity and restaurant context of the user.
But the MQTT subscription authorization wasn't enforcing that context when evaluating wildcard topic access.
In a multi-tenant application, these are two very different questions:
Authentication:_ Who are you?_
Authorization:_ What are you allowed to read?_
The first one worked.
The second one was effectively missing at the MQTT wildcard boundary.
Why This Is Dangerous In A Multi-Tenant System
MQTT is particularly interesting from an authorization perspective because wildcard subscriptions can turn a small permission mistake into a huge visibility problem.
Imagine the legitimate application needs:
restaurant-A/*restaurant-A/*But the authorization layer effectively grants:
##That's not a small increase in access.
That's a completely different security boundary.
A restaurant account should never be able to turn itself into a passive subscriber to unrelated tenants simply by changing the subscription filter.
The fact that publishing was blocked doesn't solve this.
A read-only firehose is still a firehose.
The Scale Is The Important Part
One of the biggest lessons from this issue was the importance of measuring impact.
It's easy to say:
"I can subscribe to a wildcard topic."
It's much stronger to demonstrate:
351 messages / 60 seconds
8,288 messages / 5 minutes
147,817 messages / 60 minutes351 messages / 60 seconds
8,288 messages / 5 minutes
147,817 messages / 60 minutesAnd then break those messages down into:
23,929 unique tenant/restaurant combinations
~23,000 unique OrderIds
3,777 drivers
12 countries23,929 unique tenant/restaurant combinations
~23,000 unique OrderIds
3,777 drivers
12 countriesThe numbers demonstrate that this wasn't an isolated edge case.
It was a systemic authorization failure in a shared production event bus.
How I Validated It Without Turning It Into A Data Dump
There is an important line between proving a vulnerability and collecting everything you can possibly collect.
I deliberately stayed on the proof side of that line.
For example, once I confirmed that unrelated restaurants were appearing in the stream, I didn't need to permanently store their entire activity.
Once I confirmed that fresh OrderIds could be chained into the order API, I didn't need to retrieve thousands of orders.
A few fresh examples were enough.
For the public write-up, I've gone one step further and removed:
- Production hostnames
- Real tenant names
- Restaurant identifiers
- Real OrderIds
- Driver names
- Internal project identifiers
- Authentication material
- Raw production logs
- Country-to-tenant mappings
The numbers remain because they demonstrate scale without exposing the underlying production data.
Final Thoughts
The funny part is that there was nothing particularly exotic here.
No zero-day.
No RCE.
No fancy cryptography attack.
No kernel exploit.
It was basically:
Find MQTT
↓
Try #
↓
SUBACK 0x00
↓
Wait...
↓
Why am I receiving everyone else's events?Find MQTT
↓
Try #
↓
SUBACK 0x00
↓
Wait...
↓
Why am I receiving everyone else's events?That's one of the things I enjoy about web security.
Sometimes the vulnerability isn't hidden behind some insane exploit chain.
Sometimes it's sitting in a policy file quietly saying:
Allow: *Allow: *and nobody noticed.
A restaurant account should see its restaurant.
Not 23,929 restaurant contexts.
A live-order dashboard should receive the events it needs.
Not the entire MQTT namespace.
And an OrderId should identify an order.
It shouldn't be the password for reading it.
I went looking for an interesting API endpoint.
Instead, I accidentally found the world's least useful restaurant reservation system:
I could see everyone's orders.
Back to the JavaScript.
Hopefully the next 25 MB is just fonts.