About a month ago I sent Bybit a report through HackerOne. They run a public program. The endpoint I reported leaks the kind of stuff you don't really want leaked from a crypto exchange, but the report got closed as Not Applicable. I told the triager I'd be writing about this publicly so people who care about security work know I do this stuff. He didn't object. So here we are.
I'll let my bias be known up front: I think the rejection was wrong. I'll make the case below. Let's get into it.
The Bug
The endpoint is on Bybit's fiat flow:
POST /x-api/fiat/private/channel/user_check
content-type: application/json
{"email": "[email protected]"}
You need to be logged in to hit it (cookies plus an x-session-token). With a valid session, it behaves like a "does this email belong to a Bybit user?" check. That's not actually the problem. The problem is what comes back.
If the email exists:
{
"ret_code": 0,
"ret_msg": "OK",
"result": {
"user_id": "612048937",
"nick_name": "tay***@****",
"email": "[email protected]",
"kyc": { "status": "Success" },
"is_merchant": false
}
}
If it doesn't:
{
"ret_code": 200111102,
"ret_msg": "User not found",
"result": null
}
Different status codes inside the body, different response shape, different size. You can tell them apart without even reading the JSON. Feed a list of emails into a loop and you get back two sorted lists: people who are on Bybit, and people who aren't.
Why this isn't just plain "user enumeration"
Enumeration reports get pushed back on all the time. Fair enough. Knowing an email is registered somewhere is rarely catastrophic by itself. But two things make this one different.
1. The endpoint hands out KYC status.
kyc.status: "Success" means the account has cleared identity verification. So you don't just know the email is on Bybit. You know it belongs to a verified crypto user, with real money, who has handed real ID documents to a real exchange. That's the filter a spear-phisher wants before they bother writing the first tailored message.
This isn't "a username". It's a confirmed, KYC-cleared financial profile, keyed to an email. Under GDPR, that's the kind of derived personal data regulators don't joke around about. Under the UAE PDPL, which directly governs Bybit's Dubai operations, it's the same story. Verification status sitting next to an identifier is sensitive on purpose.
2. The user_id is the key that fits the rest of the doors.
The response also leaks user_id: 612048937. Internal numeric IDs are what you'd start with if you wanted to look for IDOR bugs anywhere else on the platform. Most authenticated endpoints on a financial app are scoped by "your user". A leaked user_id is the parameter you start dropping into every account, support, transfer, and settings endpoint you can find.
In a well defended product, none of those endpoints let you pivot. In practice, on apps this size, at least one usually does. The enumeration leak is what makes that search cheap enough to be worth doing.
So this isn't really one bug. It's a primitive that makes the next bug much easier to find.
What HackerOne Sent Back
The report sat in triage for five days, then got closed:
Although your finding might appear to be a security vulnerability, after reviewing your submission it appears this behavior does not pose a concrete and exploitable risk to the platform in and on itself. If you're able to demonstrate any impact please let us know, and provide an accompanying working exploit.
Usernames are not considered sensitive information and enumeration of users has no direct impact on any of the components of the CIA triad.
Two things stand out to me.
First, "usernames are not considered sensitive." Maybe so, in general. But the response payload here is email, KYC status, internal user_id, and merchant status. Calling that "usernames" is a pretty loose summary of what the endpoint actually returns.
Second, "provide an accompanying working exploit." That's a fine bar for severity. It's not a fine bar for applicability. "Enumeration leads to spear-phish leads to account compromise" is a chain that shows up in incident write-ups every other quarter. Asking the researcher to walk the whole chain before you'll acknowledge the first link is how cheap, high-leverage defenses get deferred for years.
What a Fix Looks Like
Three things, lifted from the mitigation section of the original report:
- Normalize the responses. Exists and doesn't-exist should be indistinguishable. Same status code, same shape, response time within tolerance. Every reasonable auth flow does this.
- Drop the sensitive fields. The endpoint is consumed by one internal flow (
/bybitpay/dashboard/send/). That flow doesn't need user_id or kyc.status. It needs "ok, the recipient is reachable". A{ok: true}would have done the job. - Rate-limit hard, alert harder. If chatty responses have to stay for legitimate users, at least make a million-row enumeration loud.
The change isn't expensive. The cost of letting it stay is paid by Bybit users.
On Disclosure
I told the H1 triager I'd be writing about this publicly, and offered to take the post down if Bybit had a concern. No objection came back. Everything here is either in the original report or visible from any authenticated session on bybit.com. I'm not publishing anything that would let someone do this more efficiently than they could by reading the JavaScript on the Bybit Pay page themselves.
If Bybit changes their mind on the bug, or on this post, my contact is on the contact page.
Hope you got something out of this. More security write-ups coming.