June 25, 2026
How an Unsigned JWT Parameter Led to a $10,500 Full Account Takeover
JSON Web Tokens (JWTs) are the unsung heroes of modern stateless authentication. They allow applications to verify user identity securely…

By Tanvi Chauhan
3 min read
JSON Web Tokens (JWTs) are the unsung heroes of modern stateless authentication. They allow applications to verify user identity securely without querying a central database for every single HTTP request.
To ensure safety, a JWT consists of three parts: a Header, a Payload, and a Cryptographic Signature. The server creates the signature using a private key, guaranteeing that a user cannot tamper with their own data.
But a cryptographic tool is only as strong as the code surrounding its implementation. This is the story of how a leading corporate travel management platform — let's call them VoyageCorp — implemented secure JWTs but completely forgot to validate the parameters inside the verification layer, resulting in a $10,500 bug bounty reward.
The Target: The Session Cookie
VoyageCorp allows businesses to manage employee flights and hotel bookings. When a user logs in, the platform issues an authentication cookie containing a standard JWT.
I logged into my testing account and intercepted the dashboard loading sequence using Burp Suite. The authorization header carried a classic three-part token separated by dots:
HTTP
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTk5MjEiLCJyb2xlIjoidXNlciIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTk5MjEiLCJyb2xlIjoidXNlciIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cI decoded the token using a standard base64 parser to examine the structure:
- Header:
{"alg":"HS256","typ":"JWT"}(Specifies the HMAC-SHA256 signature algorithm) - Payload:
{"user_id":"19921","role":"user","email":"test@example.com"} - Signature:
[Raw Binary Cryptographic Hash]
If I tried to change the user_id in the payload from 19921 to an administrative ID (10001), the signature would no longer match the altered content. The backend server would detect the modification, discard the token, and log me out. The cryptographic seal worked perfectly.
The Footprint: Locating the Alternate Parameter
While mapping the platform's profile settings, I noticed a feature that allowed users to toggle between different regional company divisions (e.g., VoyageCorp US vs. VoyageCorp UK).
When a user switches divisions, the browser sends a POST request to update the profile session:
HTTP
POST /api/v2/session/switch-context HTTP/1.1
Host: app.voyagecorp.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi... [My Valid JWT]
{
"target_division": "DIV_002",
"account_id": "19921"
}POST /api/v2/session/switch-context HTTP/1.1
Host: app.voyagecorp.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi... [My Valid JWT]
{
"target_division": "DIV_002",
"account_id": "19921"
}The server processed this request and returned a brand new JWT reflecting the updated organizational context.
Look closely at that JSON body parameter: "account_id": "19921". Why was the client-side browser explicitly declaring its own account_id if the server could already extract the authenticated identity straight from the signed JWT?
This is a classic architectural anti-pattern: duplicate sources of truth.
The Twist: Parameter Blindness in the Token Generator
I wanted to see what the server did with the arbitrary "account_id" string passed in the raw JSON body. I intercepted the context-switch request and changed the body parameter to target a secondary test account ID (19935):
HTTP
POST /api/v2/session/switch-context HTTP/1.1
Host: app.voyagecorp.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi... [My Valid JWT]
{
"target_division": "DIV_002",
"account_id": "19935"
}POST /api/v2/session/switch-context HTTP/1.1
Host: app.voyagecorp.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi... [My Valid JWT]
{
"target_division": "DIV_002",
"account_id": "19935"
}I hit send. The server didn't throw an error. It didn't check my JWT payload identity against the requested body parameter. Instead, it returned a 200 OK along with a shiny new JWT:
JSON
{
"status": "success",
"token": "eyJhbGciOiJIUzI1NiM... [New Spoofed JWT]"
}{
"status": "success",
"token": "eyJhbGciOiJIUzI1NiM... [New Spoofed JWT]"
}I base64-decoded this new token and stared in disbelief at the generated payload:
JSON
{
"user_id": "19935",
"role": "user",
"email": "victim_test@example.com"
}{
"user_id": "19935",
"role": "user",
"email": "victim_test@example.com"
}The token was fully signed and authorized by VoyageCorp's master private key, but it contained the victim's identity layout.
The Exploit: Harvesting Valid Admin Tokens
The engineering oversight here was simple but devastating. The authentication architecture handled validation in two completely isolated phases:
- The API Gateway Checked the Signature: The gateway received my original token, confirmed the signature was valid, and passed the request down to the internal session-management microservice.
- The Session Microservice Generated the New Token: The internal service assumed that since the request passed the gateway, the client was fully trusted. Instead of reading the user ID inside the validated JWT payload, the token generator blindly read the unsigned
account_idparameter directly from the raw HTTP JSON body and minted a fresh token based entirely on that value.
By dropping this new, legitimately signed token into my browser's local storage, I instantly refreshed the dashboard. The application welcomed me as user 19935. I had full read/write access to their travel logs, corporate itineraries, and saved business credit cards.
Because account IDs were sequential numbers, an attacker could easily automate an enumeration script to cycle through IDs, generating functional authorization tokens for every single enterprise client on the platform.
The Remediation and Payout
I immediate halted testing, wrote a minimal proof-of-concept showing how to obtain a valid token for another user ID, and submitted the finding to VoyageCorp's private bounty program.
- Submission: Tuesday, 8:00 AM
- Triaged as Critical (P1): Tuesday, 10:15 AM
- Hot-Fix Applied: Tuesday, 1:00 PM
- Bounty Awarded: $10,500
VoyageCorp fixed the issue by deprecating the account_id body parameter completely from the endpoint schema. The session-management microservice was refactored to extract identity context only from the cryptographic claims verified inside the API gateway's authenticated user payload.
Core Lessons
- For Developers: Never allow client-side body parameters to override authenticated session parameters. If an operation requires a user ID or an account context, always extract that value directly from your verified, decrypted server-side session token (JWT payload). Treat all client-supplied JSON keys as hostile and unverified, regardless of whether the request carries a valid signature wrapper.
- For Bug Hunters: Look for redundant parameters. If an application requires a signed cookie or bearer token but also passes user IDs, email addresses, or role variables inside the raw JSON body or URL path parameters, test them immediately. These secondary parameters are often parsed by downstream microservices that don't talk to the validation engine correctly.
Have you ever found an application that completely ignored its own cryptographic protections? Let's talk about it in the comments below! If you found this write-up useful, drop some claps and follow along for more deep-dives.
𝒯𝒶𝓃𝓋𝒾 ♡