For web-based applications, Timing-based Username Enumeration is a great find. For testers it’s low-hanging fruit and a great way to enumerate valid accounts for password attacks or social engineering. For engineers, fixing can be a pain in the rear end. Recently, I had an interesting debate with a coworker after writing a re-test report for a timing-based username enumeration finding.
What’s the difference between a “Resolved” fix and a “Mitigated” fix?
The question was an interesting thought experiment and I quickly went to whip up a quick NodeJs application to test the difference.
First, what is “Timing-based Username Enumeration”?
Timing-based Username Enumeration occurs when an attacker is able to discern the difference between a valid and invalid user account for an application purely based on timing — think of login pages and password reset pages. While modern applications should always return a generic message when an account cannot be found, timing is always interesting because it’s possible for applications to take longer to return a HTTP response for valid accounts than invalid accounts.
Consider the following screenshot of our sample app where the
user parameter set to
true simulates a valid user existing in a database. Note the response time of
Now, consider a scenario where the user does not exist —
Notice the difference in roughly 1000ms! This tells us that whenever a valid user is found, the server takes roughly an extra 1000ms to respond. Knowing this, usernames or emails could be supplied to find valid users of a website — not great for security.
The bottom line, in my own opinion, would be that password reset, user registration functionality, and user signup functionality should be as asynchronous as possible. This means no blocking code to return a proper HTTP response to create a noticeable difference to an attacker.
So, what’s a “Mitigation” versus a “Fix” in this scenario? Let’s consider the logic first where a
setTimeout() is used to simulate a time delay as if a valid user were found.
To start with a mitigation, one way would be to match the timing of the server response. Since it might not be easy or possible to simply change the database operations if a user was found, you can time the response to differ if a user isn’t found. A workaround would be to set a timeout to match a valid user.
While simple, and would apply to this specific scenario, the timing when a user isn’t found is much closer to when a user is found.
As an anecdote, adding some degree of randomness is much better than a set 1000ms.
The problem here is that a fix like this doesn’t take into account factors outside of an application’s codebase controls. What happens if your network slows down when reaching the database? What happens at scale with more users in the database? There could be plenty of factors outside of your control that could tip off an attacker.
So, what’s a better fix?
If possible, like mentioned earlier, push your functions to be asynchronous whenever possible. This allows functions to continue in the background while not exposing information to a would-be attacker.
For our simple app, an additional asynchronous function was created to “background” the work it needs to do.
This allows the server to respond as quickly as it can, as fast as if a user wasn’t found.
You can also see the timing of events in the Node console. The server sends a response to the client first before processing its backend work.
All things considered, I’ll admit there’s no “one fix for all” solution here, and there’s a lot to consider when addressing an issue like this, but I hope it helps someone somewhere who’s facing an issue like this in a penetration test report or bug bounty report.