I have a Rails application that needs to allow a separate application to direct a signed-in user to our application along with a signed JWT, create a session in our application for the user, and then redirect the user to one of various pages within the application. Open redirects are bad, so I'm trying to figure out if there are accepted best practices (or even just good suggestions) to deal with restricting the redirect URL in Rails.
One wrinkle is that the approach should ideally work for an application deployed within a subdirectory. That is, redirects to a location on the same server but outside of the application's root directory should be blocked. This isn't essential, though.
Things I've looked into:
- Testing the user-supplied URL to see if it matches one of the application's routes. Unfortunately, I haven't been able to find a well-supported method to do this.
ActionDispatch::Routing::RouteSet has a
recognize_path method, but it isn't part of the public API and isn't even used by the Rails routing mechanism.
ActionDispatch::Journey::Router has a
recognize method that likewise isn't directly called by the Rails routing mechanism (though it has very similar logic) and also isn't part of the public API. And it has a
find_routes method that is used by the routing mechanism but is a
- Checking that the user-supplied URL starts with the domain and path of the application. This isn't too bad but gets tricky with subdirectory deployment and the possibility of directory traversal (
URI(root_url).merge(params[:url]) seems to resolve the directory traversal and can just be checked to see if it starts with
root_url, so this could work. But I noticed that
/%2E%2E/ will be ignored by
merge yet still treated as
/../ by Apache and Chrome (at the very least). I'm not sure if there are other, even less obvious edge cases.
If there's a better approach to this kind of situation than having a redirect URL as a user-provided parameter, I'm happy to hear that too. While it isn't essential that every page in the Rails application is accessible through this method, I also don't want to put in a lot of effort every time another page needs to be allowed.