While reviewing parts of the MyTreda codebase recently, I came across a security issue that wasn't causing any visible problems. The app worked exactly as expected: users logged in and got redirected back to where they meant to go. Everything seemed fine.
But after taking a closer look, I realized there was a hidden security risk sitting inside the authentication flow.
The Setup
Like many web applications, MyTreda allows users to be redirected after authentication. A typical flow looked something like this:
/auth/login?callbackUrl=/dashboard
After a successful login, the application would redirect the user to the URL specified in callbackUrl. This creates a smoother user experience because users return directly to the page they were trying to access. The implementation seemed straightforward.
The Problem
The issue was that the application trusted the value provided in callbackUrl. That meant an attacker could potentially create a URL like this:
/auth/login?callbackUrl=https://malicious-site.com
The flow would look like this:
- The user clicks the link.
- The user sees the legitimate MyTreda login page.
- The user logs in successfully.
- The application redirects them to the attacker's website.
The login is real, and the domain is trusted. The only part that isn't legitimate is where you actually end up.
This type of vulnerability is known as an Open Redirect.
Why Open Redirects Matter
At first glance, an open redirect may not seem serious. After all, the attacker isn't gaining access to your database or bypassing authentication.
But open redirects are often used in phishing attacks. An attacker can leverage trust in your domain to convince users that a malicious link is legitimate.
Instead of sending users directly to:
https://malicious-site.com
they can send:
https://mytreda.com/auth/login?callbackUrl=https://malicious-site.com
Many users will trust the second URL because it starts with a legitimate domain. That trust can be exploited.
How I Found It
I wasn't responding to a bug report or investigating a security incident. I was simply reviewing old code.
As products evolve, it's easy for assumptions made during development to remain unnoticed. In this case, the assumption was:
The callback URL will always be one of our routes.
Unfortunately, assumptions are not validation. Any value supplied by a user should be considered untrusted until proven otherwise.
The Fix
The solution was simple. Instead of allowing arbitrary URLs, I restricted redirects to internal application routes only.
For example:
✅ Allowed:
/dashboard
/products
/settings
❌ Rejected:
https://malicious-site.com
http://example.com
//attacker-site.com
One particularly important case was rejecting URLs that start with //. Although they look like relative paths, browsers interpret them as protocol-relative URLs that point to external domains.
A simplified version of the validation looked like this:
function isValidCallbackUrl(url: string): boolean {
return url.startsWith('/') && !url.startsWith('//');
}
Now users can only be redirected to routes within the application.
Lessons Learned
This bug reminded me of something important: not every important issue breaks functionality.
The application worked perfectly. Users weren't reporting problems, and nothing looked wrong from the outside. Yet there was still a security vulnerability waiting to be exploited.
When maintaining software, it's easy to focus on:
- features
- performance
- bug fixes
- UI improvements
But security often lives in the assumptions we forget to question, and sometimes the most valuable fixes are the ones users never notice.
Final Thoughts
Shipping features is only half the job. The other half is revisiting decisions you already made.
As MyTreda grows, I'm spending more time auditing existing code, improving security, and reducing risk alongside shipping new features.
This fix took only a few minutes to implement. Finding it required slowing down and looking at the code with fresh eyes.
And sometimes, that's where the most important improvements come from.
Have you ever found a security issue in your own code that had been hiding in plain sight? I'd love to hear about it in the comments.
Top comments (0)