Skip to content
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

Merged
merged 11 commits into from
May 2, 2024

Conversation

murny
Copy link
Contributor

@murny murny commented Mar 29, 2024

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.

Copy link
Member

@pgwillia pgwillia left a 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)
Copy link
Member

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?

Copy link
Contributor Author

@murny murny Apr 8, 2024

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 🤔

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will make this change 🙏

@murny murny force-pushed the fix-unsafe-redirect-errors branch from 2010ccd to 9768102 Compare April 17, 2024 20:18
@@ -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,
Copy link
Contributor Author

@murny murny Apr 25, 2024

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.

Copy link
Member

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?🙃

Copy link
Contributor Author

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?
Copy link
Contributor Author

@murny murny Apr 30, 2024

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)
Copy link
Contributor Author

@murny murny Apr 30, 2024

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

CHANGELOG.md Outdated Show resolved Hide resolved
pgwillia
pgwillia previously approved these changes May 1, 2024
Copy link
Member

@pgwillia pgwillia left a 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]>
@murny murny merged commit 49f709e into master May 2, 2024
4 checks passed
@murny murny deleted the fix-unsafe-redirect-errors branch May 2, 2024 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants