June 30, 2026
How an Unchecked Server-Side Parameter Injection Earned a $6,500 High-Severity Payout
When testing web applications, we often treat backends like a unified black box. We send data, and it spits an answer back. In reality, a…

By Tanvi Chauhan
3 min read
When testing web applications, we often treat backends like a unified black box. We send data, and it spits an answer back. In reality, a modern web app is a sprawling web of internal microservices, often passing your input down a chain of hidden internal APIs.
The danger arises when an application trusts user input enough to pass it directly into the query structure of a downstream service without proper sanitization. This is known as Server-Side Parameter Injection (SSPI).
This is the story of how a major global HR and payroll provider — let's call them PayPeople — exposed their internal enterprise database because an edge API endpoint blindly trusted a sorting parameter, leading to a $16,500 bug bounty reward.
The Target: The Employee Directory
PayPeople provides an administrative interface where corporate HR managers can search, filter, and sort through thousands of employee profiles.
While auditing the directory interface, I intercepted the HTTP request triggered whenever a manager clicks the column header to sort employees by their hire date or last name. The request looked like this:
HTTP
GET /api/v2/directory/employees?search=john&sort=lastName&direction=asc HTTP/1.1
Host: app.paypeople.com
Authorization: Bearer eyJhbGciOi...GET /api/v2/directory/employees?search=john&sort=lastName&direction=asc HTTP/1.1
Host: app.paypeople.com
Authorization: Bearer eyJhbGciOi...The response returned a standard JSON array of employee objects sorted alphabetically.
I tested the standard SQL injection payloads on the search parameter (e.g., ' OR 1=1--), but it was heavily sanitized and parameterized. The backend framework completely neutralized any attempts to break out of the string context.
Then, I looked closer at the sort=lastName parameter.
The Footprint: Testing Parameter Reflection
In many modern backends, developers map sorting parameters directly to internal database keys or downstream API queries.
I decided to see how the server reacted to unexpected values in the sort field. I changed sort=lastName to an arbitrary string like sort=blueSky.
The server didn't return a database error. Instead, it returned a 500 Internal Server Error with a distinct error message from a downstream microservice:
JSON
{
"error": "Downstream query failure",
"details": "Invalid field 'blueSky' for internal resource lookup."
}{
"error": "Downstream query failure",
"details": "Invalid field 'blueSky' for internal resource lookup."
}This error indicated that the edge API was taking my sort value and appending it directly into an internal backend query string before passing it to an isolated microservice.
What if I could inject parameter delimiter characters — like an ampersand (&)—into that sorting string to forge completely new parameters inside the internal query?
The Twist: Breaking the Internal Query Chain
Let's deduce how the edge server was likely constructing the internal URL request to the downstream microservice. It probably looked something like this:
https://internal-service.local/v1/records?query=john&orderBy=lastName&dir=asc
If the edge server blindly concatenates my input into that URL string without URL-encoding the special characters, I can inject an ampersand (&) to terminate the orderBy parameter and start an entirely new internal parameter.
I modified the query parameter in Burp Suite to include a URL-encoded ampersand (%26) followed by an internal parameter guess:
HTTP
GET /api/v2/directory/employees?sort=lastName%26internalParam=true HTTP/1.1
Host: app.paypeople.comGET /api/v2/directory/employees?sort=lastName%26internalParam=true HTTP/1.1
Host: app.paypeople.comIf the server properly encoded my input, it would look for a column literally named lastName&internalParam=true and throw an error.
Instead, the server responded with a normal 200 OK. The database sorted by lastName, and the trailing error disappeared. The internal service had processed &internalParam=true as a completely separate, valid configuration block. The injection was confirmed.
The Exploit: Bypassing Multitenancy Separation
Now that I could inject arbitrary parameters into the internal microservice request, I needed to see what parameters that internal service accepted.
Using the GraphQL schema insights I had gathered from an earlier recon phase of the same target, I knew PayPeople's internal microservice ecosystem relied heavily on a parameter called tenant_id to separate data between different corporate clients.
In a secure multitenant architecture, the edge API automatically appends the user's validated tenant_id to ensure they only see their own company's employees.
But what happens if the internal microservice receives two tenant_id parameters because I injected a second one?
Many HTTP query parsers handle duplicate parameters by overwriting the first instance with the second one (e.g., param=A¶m=B resolves to param=B).
I crafted a payload designed to override the hidden internal tenant filter, targeting a known competitor's tenant identifier that I uncovered during passive OSINT:
HTTP
GET /api/v2/directory/employees?sort=lastName%26tenant_id=TNT_88391_PROD HTTP/1.1
Host: app.paypeople.comGET /api/v2/directory/employees?sort=lastName%26tenant_id=TNT_88391_PROD HTTP/1.1
Host: app.paypeople.comThe edge server took that string and constructed the internal URL:
https://internal-service.local/v1/records?query=&orderBy=lastName&tenant_id=TNT_88391_PROD&dir=asc
Because my injected tenant_id parameter sat further down the query string, the internal microservice parsed it last, completely overwriting the legitimate corporate tenant restriction assigned to my session.
The Response:
The server returned a massive payload containing hundreds of social security numbers, salary details, home addresses, and bank routing info belonging to an entirely different enterprise company.
I had achieved a full cross-tenant data leak via server-side parameter injection.
The Remediation and Payout
Recognizing the severity of the data exposure, I immediately closed the data stream, truncated the response logs, and submitted a detailed vulnerability report directly to PayPeople's high-priority security queue.
- Submission: Thursday, 2:00 PM
- Triaged as Critical (P1): Thursday, 2:20 PM
- Patch Implemented: Thursday, 5:00 PM
- Bounty Awarded: $6,500
PayPeople permanently resolved the issue by applying strict URL encoding to all parameters before forwarding them to internal backend services. Additionally, they updated the internal microservice gateway to strictly reject any incoming requests containing duplicate or conflicting structural parameters.
Core Lessons
- For Developers: Never assume that internal or downstream backend services are inherently safe from injection. Any user input that is passed to an internal HTTP query, shell command, or database lookup must be strictly URL-encoded or run through an explicit whitelist validation array at the perimeter edge.
- For Bug Hunters: Pay close attention to sorting and filtering parameters that generate
500 Internal Server Errorstates. Try injecting delimiters like &, ?, or # to see if you can manipulate the structural logic of the underlying query chain.