Skip to main content

Behind the Bug

During a recent engagement, I was tasked with testing a web application, in a non-production environment, with Multi-Factor Authentication (MFA). As I began testing, I realized that all of the test accounts that had been provided for the engagement had MFA enabled. Unfortunately, none of the telephone numbers or email addresses used to receive the MFA One-Time Passcodes (OTPs) were accessible to me. While I waited to hear back from the client about linking my email or phone number to those accounts, I began poking around on the login page.

The login flow was fairly straight forward. After supplying a valid password, users were prompted to select the phone number or email address from their profile that they would like to use to receive their OTP. After selecting a method of OTP delivery, a dialog box appeared for the user to type the received OTP into.

Upon a careful inspection, a header in the response from the following POST request caught my eye. This request is submitted after a user specifies the phone or email where they would like to receive their OTP.

Request*:

POST /sendOTP HTTP/1.1
Host: subdomain.foobar[.]com
Accept: application/json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://foobar[.]com
Content-Type: application/json
authToken: [redacted]
Origin: https://foobar[.]com
Content-Length: 42
Connection: close
Cookie: [redacted]

Response* containing header:

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://foobar[.]com
OTP: 794469
Access-Control-Expose-Headers: Access-Control-Allow-Origin,authToken,Access-Control-Allow-Headers
authToken: [redacted]
Content-Type: application/json
Content-Length: 51
Date: Thu, 05 Dec 2019 02:29:47 GMT
Connection: close
Strict-Transport-Security: max-age=31536000; includeSubDomains
Set-Cookie: [redacted]


  

I was instantly intrigued. A header called ‘OTP’… could that stand for One-Time Password? It’s the correct number of digits. I quickly copied the header value of 794469, into the OTP dialogue box and pressed submit.

Request submitting OTP value:

POST /mfa/value HTTP/1.1

Host: subdomain.foobar[.]com

Accept: application/json

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate

Referer: https://foobar[.]com

Content-Type: application/json

authToken: [redacted]

Origin: https://foobar[.]com

Content-Length: 16

Connection: close

Cookie: [redacted]

Response:

HTTP/1.1 200 OK

Access-Control-Allow-Credentials: true

Access-Control-Allow-Origin: https://foobar[.]com

authToken: [redacted]

Set-Cookie: [redacted]

Content-Type: application/json;charset=utf-8

Content-Length: 259

Connection: close

Strict-Transport-Security: max-age=31536000; includeSubDomains

{

“userId”:”testAccount”

}

Success! I had bypassed the MFA flow using the OTP header’s value! After doing my happy hacker dance, and relishing in the fact that I could continue testing immediately – rather than waiting for an email from my client, I began to wonder why the system had been designed this way. When a bug like this is found, one that seems so glaringly obvious to a security professional, the temptation is often to blame the bug on incompetence. How could someone possibly design this? What were they thinking?! Admittedly, for a minute, that thought crossed my mind. Blame the developer.

But guess what? I used to be a developer. My husband is a developer. Many of my friends are developers. Most of these people are just as smart, if not more so than I am! Sure, it’s true that some developers stink. If we’re honest, however, so do some security professionals. Overall, most of the developers that I’ve met do the best job that they can with the tools in their arsenal – all the while juggling deadlines, ever changing business requirements, and dreaded scope creep. So, if it isn’t a question of competency, what is it?

For the MFA bypass bug, the more I dug into the site, the more convinced I became that this wasn’t simply a mistake made by an inexperienced developer. If someone had intended to send the OTP value to the browser, wouldn’t they do something with it? I couldn’t find any browser-side code that used the OTP header. If the header wasn’t being used for some sort of browser-side authentication check, was it perhaps a piece of debug functionality that hadn’t been turned off? Or could it be a design choice made specifically to allow UAT testers to test app functionality in the pre-production environment that I was using for my engagement?

As it turned out, my last guess was correct. It was, in fact, built to allow for easier testing, as well as to avoid paying for text message delivery in pre-production environments. And, honestly, as long as the OTP header stays out of production, I think that the design choice actually makes sense.

This engagement served as a reminder to me to be mindful of the reason behind bugs. Sure, some bugs may be born out of lazy code or bad practices. Just as often, however, I find that bugs are introduced by developers who are trying to do the right thing. They could be unaware of the security implications of their design choices or, as was the case with the MFA bypass issue, they are aware of the security risk but are making a tradeoff between security and business need. 

As security professionals, we need to ask ourselves what’s happening behind the scenes for a particular bug. Any bug found was the product of many design considerations, business requirements, and possibly knowledge gaps within a particular technology team. The best way to win the respect of our clients, and to enable our technology partners, is to consider all of the factors that led up to its introduction. By giving others the benefit of the doubt, and by recognizing that the majority of bugs are not born out of malice – we can gain the trust of our clients and better educate our technology partners.

*The hostnames and any identifiable information about the client have been modified or scrubbed. The client has been referred to as “foobar.com” as a placeholder in this report.