-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#3419: Fix unsafe redirect errors by using allow_other_host
#3429
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really elegant solution to a puzzle that had me stumped last week. Thanks for noticing the change in rails and following up on this! 🙏
@@ -83,7 +83,7 @@ def user_not_authorized | |||
# referer gets funky with omniauth and all the redirects it does, | |||
# so handle this sanely by ignoring any referer coming from omniauth (/auth/) path | |||
if request.referer&.exclude?('auth') | |||
redirect_to request.referer | |||
redirect_to(request.referer, allow_other_host: true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
request.referer
could be user input. Do you have concerns about this with allow_other_host: true
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we will have to look into what's the purpose of this. I believe its basically like the user went to a protected/login URL, and it will kick them back to where they came from and show them a "you must be logged in to see this" error flash message.
So if that is the case, then I think probably what we want is to use:
redirect_back_or_to(root_path)
and all this logic goes away.
This method comes from Rails and has some built in logic that basically does what we want to do...but it will only redirect to request.referrer
if its an internal URL (url from our application). If its an external URL it just takes them back to the homepage of our application.
So I think this would improve everything 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're right and redirect_back_or_to(root_path)
is what we want to use.
I think the allow_other_host: true
solution has some risk to it, especially in combination with request.referrer
, which is why the default changed in Rails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will make this change 🙏
2010ccd
to
9768102
Compare
@@ -118,7 +111,8 @@ def render_400(exception = nil) | |||
end | |||
|
|||
def redirect_back_to | |||
redirect_to session.delete(:forwarding_url) || session.delete(:previous_user_location) || root_path | |||
redirect_to(session.delete(:forwarding_url) || session.delete(:previous_user_location) || root_path, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one could probably be improved. We set both previous_user_location
and forwarding_url
in places.
previous_user_location
is done via request.fullpath
forwarding_url
is done via request.original_url
Both are the "current" url that was requested (so it should always be our url), so we really shouldn't need allow_other_host
.
As an example the one problem is https://era.library.ualberta.ca/items/3bcc9b7f-5b22-4cf4-ba4c-ede791eb3d1c/view/e35dcade-8076-487...
should be treated as an local URL and not an external URL. I assume this is being set via the forwarding_url
using the request.original_url
. But if it was instead being set with request.fullpath
then this issue should all go away.
I think just removing forwarding_url
and just using previous_user_location
always (which is a relative path) should fix this problem. We won't need allow_other_host
anymore 🤔
Will do some additional testing here to see if this is something we can do to simplifying everything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that approach. Perhaps we could do this in combination with your suggestion above for adding
rescue_from ActionController::Redirecting::UnsafeRedirectError do
redirect_to root_url
end
The redirect should always be safe so we shouldn't see this, but at least it would avoid the 500 error if we're wrong?🙃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I like this idea! Let's do that as well. Will do some further testing here but I think this is a good solution and will simplify this code at the same time 👏
else | ||
session[:forwarding_url] = request.original_url if request.get? | ||
session[:forwarding_url] = request.fullpath if request.get? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't need the full URL here, the path works just fine, and we use it for previous_user_location
anyway, so now both places are doing the same thing (and hopefully this removes the issues with the full URL sometimes being treated as an external URL that we saw in Rollbar).
@@ -118,7 +111,7 @@ def render_400(exception = nil) | |||
end | |||
|
|||
def redirect_back_to | |||
redirect_to session.delete(:forwarding_url) || session.delete(:previous_user_location) || root_path | |||
redirect_to(session.delete(:forwarding_url) || session.delete(:previous_user_location) || root_path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright lets break down this logic:
session[:forwarding_url]
is for when you go to a protected URL (like `/profile) and you are not signed in, you will get thrown back to the main root page and asked to login.
When you login successfully, we will redirect you back to this protected URL like /profile
.
session[:previous_user_location]
stores the current page you are on before you went to the "login" page. So if I was viewing a particular page and I click the login button, after logging in, I hopefully will still be looking at the particular page I was viewing a few seconds ago.
So we need both of these things as they handle similar but different logic. Both of these are now relative urls
. E.g we are storing /
or /profile
here and not full URLs. If any of these are set, then we will redirect to this location. And if not set, then we redirect to root_url. (I think previous_user_location
will almost always be set though like 99% of the time, unless you just went directly to the login page which I suppose we would fallback to root_path
here 🤔).
So we should never be dealing with external URLs here and hence no need for allow_other_host
argument
return if read_only_mode_enabled? | ||
|
||
@current_user = user | ||
session[:user_id] = user.try(:id) | ||
|
||
# rubocop:disable Style/GuardClause |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Saw this disable guard clause, so tried to refactor it.
Seems like we can just move the user
check up. If we are given a user
argument that is blank, then bail, otherwise @current_user
is set with the user
and everything else should just work fine now.
@@ -15,6 +15,10 @@ class ApplicationController < ActionController::Base | |||
|
|||
rescue_from JupiterCore::SolrBadRequestError, with: :render_400 | |||
|
|||
rescue_from ActionController::Redirecting::UnsafeRedirectError do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the catchall, if we somehow are still redirecting to an external URL (which shouldn't be happening anymore, as the places where this was current problem are now relative URLs now) then we will simply just redirect back to the login page. This way, no more allow_other_host
unless you absolutely know what your doing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me 👍 I feel good about not going the allow_other_hosts
route. Thanks for thinking this through :)
Co-authored-by: Tricia Jenkins <[email protected]>
This should be a quick fix to the following issues:
Both having to do with Rails 7 new logic to prevent "unsafe redirects" to external websites.
In the first issue, the external website appears to be ERA's domain such as
https://era.library.ualberta.ca/items/3bcc9b7f-5b22-4cf4-ba4c-ede791eb3d1c/view/e35dcade-8076-487...
, so perhaps maybe just need to make this recognized as a internal URL instead?In the second case it's our OAuth flow so the website is redirecting to
https://login.ualberta.ca/
Both of these seem safe (since we are setting these urls and its not coming from user input). So
allow_other_host
seems like a quick fix to resolve these issues.But, curious if there is a better way to handle this? Like, could you "approvelist" these domains/urls? (Doesn't appear to be a way to do this from first glance).
Also, maybe we should more gracefully handle these errors in the future: rails/rails@c3758a7
But these seem to be the last
redirect_to
that could be "external" urls so maybe don't need this.UPDATE: We ended up refactoring both use cases to just "redirect" better, so we shouldn't need the
allow_other_host
argument anymore. Also added a catch all that would just redirect back to root page if we somehow missed something.