June 13, 2026
How I Bypassed a Billion-Dollar Fintech’s MFA with a Single Content-Type Twist
We’ve all seen the classic bug bounty write-ups. “How I found a $10,000 Remote Code Execution by staring at a base64 string for 72 hours.”…
Tanvi Chauhan
4 min read
We've all seen the classic bug bounty write-ups. "How I found a $10,000 Remote Code Execution by staring at a base64 string for 72 hours." They are brilliant, but they often make it seem like you need a Ph.D. in cryptography to find anything substantial these days.
But every now and then, you stumble across a bug that makes you pause, blink at your screen, and realize that the most secure systems on the planet are still built by tired humans.
This is the story of how a standard, heavily audited Multi-Factor Authentication (MFA) system collapsed because of a single, unexpected line in an HTTP header.
The Target: The Vault
The target was a massive fintech platform — let's call them SecurePay. They had a public bug bounty program with top-tier payouts, a hardened attack surface, and a security team that patched vulnerabilities faster than you could draft the report.
I had spent three days mapping their API endpoints with nothing to show for it but a collection of 403 Forbidden and 401 Unauthorized statuses. They used strict JSON web tokens (JWTs), robust rate-limiting, and an MFA system required for every single sensitive action (like changing a payout bank account).
If you wanted to move money, you needed to pass the MFA checkpoint. No exceptions.
The Footprint: Analyzing the MFA Flow
I decided to intercept the MFA verification traffic using Burp Suite. When a user inputs their 6-digit SMS or Authenticator code, the browser sends a POST request that looks something like this:
HTTP
POST /api/v2/mfa/verify HTTP/1.1
Host: api.securepay.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
{
"mfa_token": "REQ_987654321",
"code": "123456"
}POST /api/v2/mfa/verify HTTP/1.1
Host: api.securepay.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
{
"mfa_token": "REQ_987654321",
"code": "123456"
}If the code is wrong, the server returns a crisp, unyielding error:
JSON
{
"status": "error",
"message": "Invalid verification code."
}{
"status": "error",
"message": "Invalid verification code."
}Standard stuff. I tried the usual tricks:
- Rate-limiting bypasses: Adding headers like
X-Forwarded-For: 127.0.0.1to spoof my IP. (Blocked). - Array injection: Changing
"code": "123456"to"code": ["123456", true]. (Resulted in a400 Bad Request). - Type juggling: Sending the code as an integer instead of a string. (No dice).
The backend parser was strict. It expected JSON, validated it against a schema, and rejected anything else. Or so I thought.
The Twist: Content-Type Shifting
While taking a break, I started thinking about the backend architecture. SecurePay's tech stack was a hybrid of a modern Node.js API gateway routing requests to legacy SOAP and XML-based internal services.
When a framework handles multiple data formats, it relies heavily on the Content-Type header to decide which parser to invoke.
What happens if we force the application to use a different parser entirely, but keep the payload the same? Or better yet, what if we switch to an older format like application/x-www-form-urlencoded?
I loaded the request back into Burp Suite's Repeater and made two crucial changes.
- I changed the
Content-Typeheader toapplication/x-www-form-urlencoded. - I converted the JSON body into standard URL-encoded parameters.
The request now looked like this:
HTTP
POST /api/v2/mfa/verify HTTP/1.1
Host: api.securepay.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOi...
mfa_token=REQ_987654321&code=123456POST /api/v2/mfa/verify HTTP/1.1
Host: api.securepay.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOi...
mfa_token=REQ_987654321&code=123456I hit send. 400 Bad Request. The parser explicitly required JSON.
But then I thought: What if the API gateway validates the JSON, but the internal MFA microservice parses it differently? What happens if I lie to the server? I kept the body as JSON, but told the server it was form-urlencoded.
HTTP
POST /api/v2/mfa/verify HTTP/1.1
Host: api.securepay.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOi...
{"mfa_token": "REQ_987654321", "code": "123456"}POST /api/v2/mfa/verify HTTP/1.1
Host: api.securepay.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOi...
{"mfa_token": "REQ_987654321", "code": "123456"}Suddenly, the error message changed. It wasn't a 400 Bad Request anymore. It was a 500 Internal Server Error with a stack trace revealing a PHP-based backend module trying to read a null object.
This meant the strict JSON validation layer was being bypassed because the API gateway saw the Content-Type: application/x-www-form-urlencoded and skipped the JSON schema check, passing the raw string directly to the internal microservice.
The Vulnerability: The Empty Parameter Exploit
Since the internal service was expecting URL-encoded data but received raw JSON text, its internal parameter parser failed to find the keys mfa_token and code in the traditional format.
In PHP and certain legacy frameworks, if a form parser looks for a variable that isn't provided, it initializes it as null or false.
If the backend code looked something like this:
PHP
$user_code = $_POST['code']; // Evaluates to NULL because the body wasn't URL-encoded
$correct_code = getStoredMFACode($token);
if ($user_code == $correct_code) {
echo json_encode(["status" => "success"]);
}$user_code = $_POST['code']; // Evaluates to NULL because the body wasn't URL-encoded
$correct_code = getStoredMFACode($token);
if ($user_code == $correct_code) {
echo json_encode(["status" => "success"]);
}Notice the loose comparison operator (==) instead of a strict comparison (===). In legacy PHP, null == "" or null == false can evaluate to true.
I realized that if I sent a completely empty body, or a body that the form-parser couldn't read, $user_code would resolve to null. If the getStoredMFACode function also failed or returned an empty state under certain conditions (like an expired token or an uninitialized session), null == null would evaluate to true.
I stripped the body entirely, kept the Content-Type as application/x-www-form-urlencoded, and hit send:
HTTP
POST /api/v2/mfa/verify HTTP/1.1
Host: api.securepay.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOi...
Content-Length: 0POST /api/v2/mfa/verify HTTP/1.1
Host: api.securepay.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOi...
Content-Length: 0The Response:
HTTP
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"mfa_bypass_token": "BIZ_SECURE_99a8b7c6..."
}HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"mfa_bypass_token": "BIZ_SECURE_99a8b7c6..."
}My jaw dropped. The server returned a 200 OK. By sending an empty request with a misleading Content-Type header, the authentication logic short-circuited, assumed the non-existent code matched the non-existent backend value, and granted me a valid session token.
I had completely bypassed MFA.
The Impact and Bounty
With this bypass token, an attacker who had compromised a user's password (or stolen a session cookie) could bypass the secondary security layer entirely, change the user's bank details, and drain the account. It was a textbook Critical-severity flaw.
I immediately documented the steps, recorded a quick proof-of-concept video, and submitted it to SecurePay's bug bounty program.
- Submission: 11:42 PM
- Triaged: 8:15 AM the next morning (Confirmed as Critical).
- Fix Deployed: 2:30 PM (Strict type checking added, explicit
Content-Typeenforcement applied at the gateway). - Bounty Awarded: $8,500
Key Takeaways for Developers and Pentesters :
This bug highlights a massive blind spot in modern application security: impedance mismatch. When different layers of an application (gateways, microservices, legacy backends) interpret headers and data bodies differently, catastrophic vulnerabilities happen.
- For Developers: Never rely solely on edge gateways for input validation. Enforce strict type checking (=== in PHP/JS) and ensure your backend services explicitly reject requests that do not strictly match the expected
Content-Type. - For Hunters: Don't just test the data inside the parameters. Mess with the structural scaffolding of the request. Flip JSON to XML, change
POSTtoPUT, mix up yourContent-Types, and watch how the backend struggles to clean up the mess.
Have you found a critical bug using simple header manipulation? Let's talk about it in the comments below! If you enjoyed this write-up, don't forget to clap and follow for more deep dives into the world of cyber security and ethical hacking.
𝒯𝒶𝓃𝓋𝒾 ♡