The original post was published on my blog on July 27, 2023, long before I realized it might be interesting to the dev.to community.
Hello!
In my previous post, I briefly touched on the latest 1.0.0-alpha.2
release of Secutils.dev. While the "Resources tracker" was the central part of this release, I want to highlight one tiny fix that makes a huge difference in user experience but can also be quite risky if not done right: “Recover original URL after sign-in”.
When you open a link to a web page that requires authentication, you are usually redirected to the login page. The most annoying thing that can happen is that after you enter your credentials and hit “Enter” you’re NOT redirected to your original destination! I see this issue more frequently on websites that require complex Single Sign-On (SSO) authentication flows, but it can also occur on sites with simple login flows. While it may be tolerable for one-time use links, it becomes a real pain if you need to use the link frequently or bookmark it.
Such behavior creates quite a bit of friction, distracting users from the action they wanted to perform initially. This drives users (myself included) crazy, and I don’t want to do that to Secutils.dev users.
There are different ways to approach this issue. The most common ones are to “remember” the original URL as a parameter in the login page URL to which the user is redirected, or to avoid redirect altogether by rendering a login modal/popup directly on the destination page. Both approaches have their pros and cons, but considering various corner cases with the login modal/popup approach, I generally prefer the separate login page for its simplicity and universality. As you might have guessed, I use this approach for Secutils.dev.
Now, let’s see how it works: If the user tries to access https://secutils.dev/ws/web_scraping__resources
but isn’t authenticated, the app will redirect them to https://secutils.dev/signin?next=/ws/web_scraping__resources
. This way, the login page can extract the original destination page from the next
query string parameter and redirect the user to it after successful login. Is this that simple? Yes and no!
If we blindly redirect the user to whatever URL is embedded in the next
query string parameter, we run the risk of becoming an easy phishing target. Imagine someone shares the following link with you: https://secutils.dev/signin?next=%2F%2Fws-secutils.dev%2Fws%2Fweb_scraping
. The main domain looks legit, and it’s not easy to notice that the next
parameter includes a URL to a completely different website - https://ws-secutils.dev/ws/web_scraping
. Malicious actors can exploit various tricks and browser quirks to conceal the real destination in the next
parameter. For example, here I used a URL-encoded protocol-relative URL (//
instead of https://
).
If Secutils.dev doesn’t validate the URL from the next
parameter properly, after successful login, you'll be redirected to https://ws-secutils.dev/ws/web_scraping
, and the chances that you'll check the URL bar again are very low. This is known as an “open redirect”. The https://ws-secutils.dev/ws/web_scraping
page can look exactly like the login page of the legitimate website and can prompt you to re-enter your Secutils.dev credentials, tricking you into providing your login details on the wrong website. After it steals your credentials, it can finally redirect you to https://secutils.dev
, and you might not even notice that something was wrong.
ℹ️ NOTE: Such phishing attacks wouldn’t work if you use passkey as your credentials since it’s bound to the legitimate website/origin. If you don’t use passkey to sign in to Secutils.dev yet, I strongly encourage you to consider switching to it. It’s easy to do and convenient to use.
Phishing is much more common than you might think. Take a look at the image with the recent statistics:
The example above shows that you absolutely have to validate all URLs you redirect users to if there is a chance they can be manipulated by third parties. In the Secutils.dev Web UI, specifically, I rely on the native URL
class to check if the URL has the proper origin before redirecting the user. Also, check out "Preventing Unvalidated Redirects and Forwards" from OWASP for more tips.
I hope I showed that seemingly unimportant and easy fixes can not only have a significant impact on the user experience but can also put your users at risk if you don’t consider the security implications of such changes. It's crucial to be mindful of potential risks and carefully validate any modifications that involve user redirection to ensure the safety of your users.
That wraps up today's post, thanks for taking the time to read it!
ℹ️ ASK: If you found this post helpful or interesting, please consider showing your support by starring secutils-dev/secutils GitHub repository.
Also, feel free to follow me on Twitter, Mastodon, or LinkedIn.
Thank you for being a part of the community!
Top comments (6)
Hey Aleh, cool project! In case you're interested, applications are still open to join the Codacy Pioneers, a fellowship for open source developers.
This program will fund, promote, and mentor innovative and creative OSS developers worldwide. The people selected will receive a year-long monthly stipend, free tools, widespread promotion, and mentorship from some of the brightest minds of today’s OSS community:
Vue framework creator Evan You;
Enix co-founder Jerome Petazzoni;
Prisma founder Johannes Schickling;
CHAOSS community lead Ruth Ikegah; and
Christoph Nakazawa, the creator of popular open-source tools like Jest, Metro, Yarn, and MooTools.
Head over to codacy.com/pioneers to learn more.
Hey @heloisamoraes, I'll check it out, thank you!
Hi, nice write up (also I need to check secutils).
I've encountered this issue but I don't think "redirect after login" is where you're most at risk to find it for two reasons :
It's pretty easy to secure (only allow redirections to your own domain). You donlt even need to do this at the application level : You can use your reverse proxy or WAF to inspect every response, make sure redirects are always the same domain as the request, and block everything else.
I wouldn't even put this in the URL to begin with. I'd just store the intended route (not even the full URL) in a session so I don't have to pass it as a param from page to page if the user also has to create an account, validate it and so on.
Where I did encounter the issue is when some clients (in a b2b app) required us to redirect users to their users to a document on their own site (not ours) and keep track of who clicked the link. So we basically built a redirect route that would accept an url as a param, log that thebuser clicked the url and redirect them... And here we can't check that the domain is the same because it won't be. I think the best approach in this case is to do something like a url shortener : store the redirect urls in a database and just pass the id as a param (but you still have to either disable the reverse proxy rule I mentionned in point 1, or keep track of every authorized redirect domain and update the config accordingly).
Hey,
Thanks for the comment!
Yep, in a simple case with a single deployment model, you can do that on the proxy level. However, you'd need to write an integration test against your real proxy to ensure your configuration works as expected after your initial setup. Otherwise, at some point, it will likely break during an upgrade, domain or sub-domain change, or some other proxy configuration refactoring. So, it might not be as cheap and easy as it seems at first.
The second reason to implement this on the application level is if you support multiple deployment models. For example, for Secutils.dev, I want to accommodate both on-premises setups (with or without WAFs, reverse proxies, etc.) and my own SaaS, where I do have a reverse proxy. In this case, I want Secutils.dev to work consistently everywhere and place as little deployment burden on on-premises users as possible.
It could work too, I think it's a matter of taste. I usually prefer not to create sessions (or any other server-side persistent data) for unauthenticated users to reduce the chance of unauthenticated DDoS attacks. You can add some acrobatics with client-side localStorage/sessionStorage, but using different mediums to store pieces of the session would be even more complicated.
I assume you also maintain a list of whitelisted domains on a per-customer/global level at least, right? If so, the attack surface might not be that big to seriously worry about it. Also, since we usually only care about security for users using browsers, a whitelist can also be bound to the HTTP Referer header (assuming the resources where links are located populate that header).
Yeah, that's another form of mapping based on whitelisted domains and outbound redirects.
That's another reason to consider doing this on application level, by the way.
Awesome-sauce! So glad ya decided to share this one with us, Aleh. 🙌
Glad you found it interesting and/or helpful!