IPL season. Everyone’s fighting for tickets. I was on District Zomato’s ticketing app (they acquired Paytm’s Insider events business and rebranded it) trying to grab seats like everyone else.
And the thing I actually noticed first wasn’t a bug it was that the queue system was genuinely nice. You hit the sale, you get put in a virtual waiting room, it holds your spot, and it lets people through in order. No refresh-spamming, no thundering herd taking the site down. I liked it.
So, old habits: I opened DevTools to see how it worked.
It’s QueueFair
The waiting room was powered by QueueFair a third-party virtual queue service. The way it works, roughly:
- You land on the sale page.
- QueueFair puts you in line and, once it’s your turn, drops a pass cookie in your browser.
- Every request to the ticketing backend carries that cookie. The server checks it and goes “yep, this person cleared the queue” and lets you buy.
Quick note before I go further: none of this is secret sauce or reverse-engineered internals. This is just how QueueFair works the cookie format and the signing scheme are documented publicly in their own developer docs. I’m not leaking anything about their design. The bug was entirely in how District deployed it.
The pass cookie is named QueueFair-Pass-<queue_id> and it’s signed. Its value looks like this:
qfqid=<session id>&qfts=<timestamp>&qfa=<account>&qfq=<queue id>&qfpt=Passed&qfh=<signature>
That qfh at the end is the important part. It’s an HMAC-SHA256 signature over the rest of the fields, keyed with a queue secret. That’s what makes the cookie un-forgeable you can’t produce a valid qfh without knowing the secret. The whole security model rests on that secret staying on the server.
Then I Read the Page Source
Here’s where it went sideways.
District’s IPL booking page ships a big JSON blob the entire tour/event config embedded right in the HTML inside a <meta content="{...}"> tag. Server-side rendering, hydration data, the usual. Nothing unusual about that by itself.
But when I pretty-printed that blob, buried deep in it, per event, per sale, was this:
"queue_configuration": {
"queue_id": "69b18d95...fe00388d69fd59e3",
"queue_secret": "jtnr…redacted…rln14w4g",
"queue_data": {
"event_name": "…",
"max_purchase_per_user": 2
}
}
The queue_secret. The one thing that was never supposed to leave the server. It was being shipped to every single visitor, in the page, in plain text.
Why That’s Bad
If you have the secret, you don’t need to wait in the queue at all. You can just mint your own valid pass cookie and walk straight to the front.
The forging logic is a few lines. Build the signed string, HMAC it with the leaked secret, set the cookie:
import hmac, hashlib, time
queue_name = "…redacted…"
secret = "…redacted…" # this was sitting in the page
user_agent = "Mozilla/5.0 …" # must match the browser's UA
qfts = str(int(time.time()))
signed = (
f"qfqid=anything123&qfts={qfts}"
f"&qfa=…&qfq={queue_name}&qfpt=Passed&"
)
sig = hmac.new(secret.encode(),
(user_agent + signed).encode(),
hashlib.sha256).hexdigest()
cookie = (
f"QueueFair-Pass-{queue_name}="
f"qfqid=anything123&qfts={qfts}&qfa=…"
f"&qfq={queue_name}&qfpt=Passed&qfh={sig}"
f"; path=/; domain=.district.in; SameSite=None; Secure"
)
print(f"document.cookie = '{cookie}'")
Paste that cookie in, refresh, and the backend thinks you’ve already cleared the waiting room. No line. No wait.
To prove it end-to-end I wrapped it in a tiny Chrome extension: it read the queue config straight off the page, listed every live sale with its queue_id and queue_secret, and had one button Inject Cookie that signed a fresh pass and dropped it into .district.in. One click, front of the queue.
The Part That Actually Scared Me
This wasn’t just “skip the line so I get a better seat.”
The queue was the only thing rate-limiting the sale. It’s what stops one person from hammering checkout. Bypass it and you can automate the whole flow spin up sessions, forge passes, and drive checkout as fast as the backend will take it.
During an IPL final, a bad actor could have effectively minted as many tickets as they wanted, front-run every genuine fan sitting patiently in the real queue, and dumped them on the resale market. The fair queue everyone was trusting would’ve meant nothing.
That’s the difference between “annoying bug” and “you can corner the entire ticket sale.”
Reporting It
I reached out on Twitter first, kept it vague no secret, no exploit, just “hey, your queue config is leaking something it shouldn’t.” They responded and pointed me to their bug bounty program to file it properly.
So I wrote it up: where the secret was exposed, how the pass cookie is signed, a redacted proof-of-concept, and the impact full queue bypass and unbounded ticket purchasing.

It got triaged, confirmed, and resolved. And they awarded me a $500 bounty for it.
Credit where it’s due: they didn’t just yank the secret out of the page and call it a day. They went through a proper remediation pass and hardened the whole flow even replaying an old, genuinely-issued “Passed” cookie doesn’t get you through anymore, along with a bunch of other tightening around how passes are validated. It was a real search-and-fix, not a one-line patch.
The Takeaway
The fix is boring, which is the point: secrets don’t belong in anything you send to the browser. Server-side rendering makes it really easy to accidentally serialize an entire config object secrets and all into the page. The queue secret should have stayed server-side and never been part of queue_configuration in the hydration blob.
I wasn’t hunting for this. I just wanted tickets, liked the queue, and got curious about how it worked. That’s usually how these things go.
If you find something like this report it, don’t exploit it, and let them fix it before you say a word publicly. Most teams will thank you for it.
Secret and queue identifiers redacted. The issue has been fixed.
Questions about API security or want to chat? Hit me up.