September 8, 2026
How I Could Have Shut Down Every Restaurant in Europe With One Click
I found a Broken Access Control vulnerability that could let one restaurant modify another restaurantβs delivery zones.
By Mohammed Ashraf
6 min read
I found a Broken Access Control vulnerability that could let one restaurant modify another restaurant's delivery zones.
Sometimes a bug starts with a complicated attack chain.
Sometimes it starts with:
"Hmm⦠what happens if I change this ID?"
This one started exactly like that.
While testing part of its authorized bug bounty program, I came across an API responsible for managing restaurant delivery areas.
At first glance, it looked pretty normal.
Then I changed a restaurant ID.
And the API basically said:
"Sure, why not?"
That was the moment things got interesting.
The Endpoint
The Partner Hub frontend uses an API similar to:
GET /v2/deliveryarea/lists/shorts/{restaurantId}GET /v2/deliveryarea/lists/shorts/{restaurantId}The frontend normally sends the authenticated restaurant's own ID.
Something like:
GET /v2/deliveryarea/lists/shorts/12345678GET /v2/deliveryarea/lists/shorts/12345678The interesting question was:
What happens if I replace that ID with another restaurant's ID?
So I changed it to a different, unrelated restaurant.
For this article, all identifiers below have been replaced with fictional values.
GET /v2/deliveryarea/lists/shorts/87654321GET /v2/deliveryarea/lists/shorts/87654321And I got:
HTTP/1.1 200 OKHTTP/1.1 200 OKWith delivery-area information containing objects similar to:
{
"id": 1876543210,
"available": 1,
"boundary": "POLYGON((24.3512 42.6901,23.3524 52.6917,24.3541 52.6912,...))"
}{
"id": 1876543210,
"available": 1,
"boundary": "POLYGON((24.3512 42.6901,23.3524 52.6917,24.3541 52.6912,...))"
}Wait.
I wasn't supposed to have access to this restaurant.
But the API returned its delivery areas anyway.
The First Finding: Cross-Tenant Read
The important part wasn't simply that the API returned an object.
It returned another restaurant's delivery configuration.
The application was effectively trusting:
restaurantId from URLrestaurantId from URLinstead of enforcing:
authenticated_user β authorized_restaurants β requested_restaurantauthenticated_user β authorized_restaurants β requested_restaurantIn other words:
My account
|
v
My restaurant
|
X
|
v
Someone else's restaurantMy account
|
v
My restaurant
|
X
|
v
Someone else's restaurantThe backend wasn't properly checking that relationship.
But There Was More
Reading another restaurant's delivery areas was already interesting.
Then I noticed something else in the JavaScript bundle.
The frontend contained mappings for the delivery-area API.
Conceptually, it looked like:
deliveryAreas:
restaurantId =>
smartGateway +
"/v1/delivery-area/list/short/" +
restaurantId
deliveryAreaEnable:
areaId =>
smartGateway +
"/v1/delivery-area/" +
areaId +
"/enable"
deliveryAreaDisable:
areaId =>
smartGateway +
"/v1/delivery-area/" +
areaId +
"/disable"deliveryAreas:
restaurantId =>
smartGateway +
"/v1/delivery-area/list/short/" +
restaurantId
deliveryAreaEnable:
areaId =>
smartGateway +
"/v1/delivery-area/" +
areaId +
"/enable"
deliveryAreaDisable:
areaId =>
smartGateway +
"/v1/delivery-area/" +
areaId +
"/disable"That immediately raised another question:
If I can obtain another restaurant's area ID, can I modify it too?
The Write Primitive
I took one of the area IDs returned by the unauthorized read.
Again, the values shown here are fictionalized.
Before the test:
available = 1available = 1Then I sent:
PUT /v2/deliveryarea/1876543210/disablePUT /v2/deliveryarea/1876543210/disableThe response:
HTTP/1.1 204 No ContentHTTP/1.1 204 No ContentA 204 by itself doesn't prove much.
So I queried the delivery area again.
Before:
available = 1available = 1After:
available = 0available = 0The backend had actually changed the state.
Not a UI trick.
Not a cached response.
Not a fake success message.
An actual backend-side state transition.
I then restored the state:
PUT /v2/deliveryarea/1876543210/enablePUT /v2/deliveryarea/1876543210/enableAnd verified:
available = 1available = 1So the complete transition was:
1 β 0 β 11 β 0 β 1Everything was restored immediately after testing.
That's When an IDOR Became Much More Serious
At this point, the issue was no longer simply:
"I can read another restaurant's data."
It was:
"I can modify another restaurant's delivery configuration."
That changes the impact considerably.
A delivery area determines where a restaurant can deliver.
If an unauthorized user can disable that area, the restaurant could potentially stop accepting deliveries from an entire geographic zone.
Imagine a restaurant operating normally.
Then someone changes:
Delivery Area A
available = trueDelivery Area A
available = trueto:
Delivery Area A
available = falseDelivery Area A
available = falseCustomers inside that area could potentially lose delivery coverage.
And the attacker doesn't need access to the restaurant's dashboard.
They don't need to log into the victim restaurant.
They don't need the victim's credentials.
They only need an authenticated partner account.
The Authorization Problem
The core issue appeared to be that the gateway was resolving the requested resource primarily from the identifier supplied in the request.
The application had contextual information such as the authenticated account and restaurant identity.
But the authorization layer wasn't properly enforcing:
Does this user actually control restaurant X?Does this user actually control restaurant X?Instead, the effective logic looked much closer to:
Authenticated?
|
+-- Yes
|
+-- Take restaurantId from URL
|
+-- Return / modify objectAuthenticated?
|
+-- Yes
|
+-- Take restaurantId from URL
|
+-- Return / modify objectThe secure model should be:
Authenticated user
|
v
Authorized restaurants
|
v
Requested restaurant
|
+-- Match? --> Continue
|
+-- No -----> 403 ForbiddenAuthenticated user
|
v
Authorized restaurants
|
v
Requested restaurant
|
+-- Match? --> Continue
|
+-- No -----> 403 ForbiddenThat authorization boundary was missing.
Why the Area IDs Matter
Another interesting detail was that the delivery-area identifiers were numeric and appeared to be discoverable through the unauthorized read functionality.
That creates a potentially dangerous chain:
Restaurant ID
β
Unauthorized delivery-area lookup
β
Area ID
β
Unauthorized enable/disableRestaurant ID
β
Unauthorized delivery-area lookup
β
Area ID
β
Unauthorized enable/disableThe attacker doesn't necessarily have to guess the area ID.
The vulnerable read endpoint can provide it.
This makes the write primitive considerably easier to automate.
Could This Be Automated?
Technically, yes.
And this is where things get a little scary.
I did not perform mass exploitation.
There was no reason to start randomly disabling real restaurants just to make a screenshot for a report.
The vulnerability itself, however, creates an automation opportunity.
Conceptually, an attacker could build a script that performs:
for each restaurant:
retrieve delivery areas
extract area IDs
disable selected areasfor each restaurant:
retrieve delivery areas
extract area IDs
disable selected areasWhich means the attack isn't necessarily limited to one restaurant.
It could potentially be scaled.
In the worst-case scenario, an attacker could automate the process to affect a large number of restaurants.
Basically:
One vulnerable authorization check
+
Enumerable identifiers
+
Write operation
=
Very bad TuesdayOne vulnerable authorization check
+
Enumerable identifiers
+
Write operation
=
Very bad Tuesdayπ
The Safe PoC
For the actual bug bounty submission, I intentionally kept the impact demonstration controlled.
I verified the state transition on a test operation and immediately restored it.
I did not attempt to disrupt multiple restaurants.
I also provided the security team with reproduction steps so they could independently validate the behavior.
This is an important part of bug bounty testing:
You don't need to burn down the house to prove that the gasoline is flammable.
Once you have demonstrated the unauthorized write and verified the backend state change, continuing to cause damage adds very little security value.
What Made This a P1?
The final issue combined multiple security problems:
1. Cross-tenant authorization bypass
An authenticated partner account could access delivery areas belonging to another restaurant.
2. Sensitive geographic information disclosure
The response exposed delivery-area boundaries, including polygon/multipolygon coordinates.
3. Unauthorized modification
The attacker could disable and enable delivery areas belonging to another restaurant.
4. Business impact
Disabling delivery areas can affect where a restaurant is able to accept deliveries.
5. Potential scalability
Because restaurant and area identifiers could be obtained and processed programmatically, the write primitive could potentially be automated across multiple targets.
That's a pretty nasty combination for what initially looked like a simple IDOR.
The Funny Part
The frontend was actually doing the right thing from a normal user-flow perspective.
It normally sent the logged-in restaurant's ID.
But security cannot depend on:
"The frontend wouldn't normally send that."
Because an attacker doesn't have to behave like the frontend.
If the browser sends:
GET /deliveryarea/lists/shorts/12345678GET /deliveryarea/lists/shorts/12345678an attacker can send:
GET /deliveryarea/lists/shorts/87654321GET /deliveryarea/lists/shorts/87654321The backend has to decide whether that request is authorized.
Not the JavaScript.
Not the UI.
Not the user.
The server.
The Fix
The obvious remediation is to enforce object-level authorization before performing the lookup or modification.
For example:
Authenticated user
β
Resolve user's authorized restaurants
β
Check requested restaurant
β
Allow / denyAuthenticated user
β
Resolve user's authorized restaurants
β
Check requested restaurant
β
Allow / denyFor write operations, the server should additionally verify that the requested areaId belongs to a restaurant the authenticated user is authorized to manage.
Something equivalent to:
areaId
β
Find owning restaurant
β
Is caller authorized for this restaurant?
β
YES β modify
NO β 403areaId
β
Find owning restaurant
β
Is caller authorized for this restaurant?
β
YES β modify
NO β 403Headers such as X-Stands-As should never be treated as a security boundary by themselves.
They can be useful for application context, but authorization must ultimately be derived from trusted server-side identity and access-control rules.
The Biggest Lesson
IDOR/BOLA vulnerabilities are often underestimated because the initial test can look ridiculously simple.
You change:
/user/123/user/123to:
/user/124/user/124and suddenly everyone thinks:
"It's just an IDOR."
But the real question isn't:
"Can I change the ID?"
The real questions are:
- What object did I reach?
- Is it cross-tenant?
- What information does it expose?
- Can I modify it?
- Does the modification have a real business effect?
- Can I chain the read primitive into the write primitive?
- Can the attack be automated?
- How many objects could theoretically be affected?
That's where a simple IDOR can turn into a serious BOLA.
And Yesβ¦ About That Button π
I didn't actually go and shut down restaurants.
That would have been a terrible idea.
But from an attacker's perspective, once you have:
Restaurant ID
β
Delivery Area IDs
β
Unauthorized disable endpointRestaurant ID
β
Delivery Area IDs
β
Unauthorized disable endpointautomation becomes the interesting part.
With a properly authorized test environment and a safe target set, the same logic could be wrapped into a script capable of processing many restaurants automatically.
So, in the most dramatic possible summary:
One authorization bug + a little automation = a hypothetical "close everything" button.
Please don't make that button. π
The important lesson isn't that I could disrupt the platform.
It's that the backend should make sure nobody without authorization can disable even one other restaurant's delivery area in the first place.
And that's exactly why object-level authorization matters.
Final
Never assume an ID in a URL is protected just because the UI only exposes your own ID.
Always test the authorization boundary:
Can I access another tenant's object?
Can I modify it?
Can I chain the two?
Can I automate it?Can I access another tenant's object?
Can I modify it?
Can I chain the two?
Can I automate it?Because sometimes the difference between a low-impact IDOR and a P1 isn't the IDOR itself.
It's what happens after you change the ID.
Happy hunting. π