-
Notifications
You must be signed in to change notification settings - Fork 678
Extras URLs vs URIs
In our discussion of how to solve the problem of Unvalidated Redirects, we chose to whitelist possible redirect URLs by making sure that they are part of our Rails application:
def post_authentication_redirect_path(default_path: home_dashboard_index_path)
path = params[:url] || default_path
Rails.application.routes.recognize_path(path)
rescue ActionController::RoutingError
default_path
end
While that code should work for our purposes, a previous version of RailsGoat actually recommended a different solution:
path = home_dashboard_index_path
begin
if params[:url].present?
path = URI.parse(params[:url]).path
end
rescue
end
This code is meant to parse a URL (such as http://evil.com/open-redirect
) and
snip the domain, so that only the path path (/open-redirect
) remains. This
seemed sufficient until a user discovered that the URI.parse
method let some
other attacks through. Here are the results of some other malicious URIs that
actually work:
[1] pry(main)> URI.parse("@evil.com/open-redirect").path
=> "@evil.com/open-redirect"
[2] pry(main)> URI.parse("////evil.com/open-redirect").path
=> "//evil.com/open-redirect"
[3] pry(main)> URI.parse("-evil.com/open-redirect").path
=> "-evil.com/open-redirect"
[4] pry(main)> URI.parse(".evil.com/open-redirect").path
=> ".evil.com/open-redirect"
If you try these URIs in your application, you'll see that your browser will
happily redirect you to the malicious site. The problem here is that our
URI.parse
method doesn't differentiate between URIs and URLs. You may not
even realize that they're different, and in this case that lack of
differentiation allows users to maliciously attack our site.
Briefly, a URL (Uniform Resource Locator) is just one type of URI (or Uniform Resource Indicator); the one we're most familiar with. A URL has a pretty well defined format, so we're used to thinking that all parseable URIs will match this format in some way. However, the other attack examples we provided are legitimate URIs that are not legitimate URLs, and take advantage of the fact that our user's browser will try to interpret those URIs as a sensible URL.
As mentioned in the original exploit, one solution to this problem is to verify that the requested URI is part of our Rails app:
Rails.application.routes.recognize_path(path)
If we need to redirect users to paths that are outside our Rails application, or to other domains, this approach won't work. In that case, another workable approach is to product a whitelist of URL patterns that are known to be good:
WHITELISTED_URLS = [
/\Ahttp:\/\/google.com\/.*\z/,
/\Ahttp:\/\/mysite.com\/.*\z/,
]
unless WHITELISTED_URLS.any? { |pattern| path =~ pattern }
raise ActionController::RoutingError
end
redirect_to path
As you can see, this approach can be hard to read and maintain. For instance,
as written, users can be redirected to google.com
, but not news.google.com
.
Sloppy regexes are also frequently insecure on their own. If you take this
approach, remember to anchor your regexes with \A and \z, or you'll likely be
attacked via your Regexes.
We can make this a little cleaner (while still fixing the URI-versus-URL issue) by using URI parse to detect properly formatted URLs:
WHITELISTED_HOSTS = [
"google.com",
"mysite.com",
]
parsed_host = URI.parse(params[:url]).host
unless WHITELISTED_HOSTS.include?(parsed_host)
raise ActionController::RoutingError
end
redirect_to params[:url]
If an attacker attempts to provide a URI like @evil.com/open-redirect
, our
new approach will return a nil
host and properly raise an error.
One final approach is to avoid being passed arbitrary URLs at all. While our
login method should be able to redirect you back to any location you came from,
this won't always be true. Whenever possible, redirect to hardcoded locations
(e.g. home_dashboard_index_path
) to avoid this attack entirely:
redirect_to home_dashboard_index_path
Sections are divided by their OWASP Top Ten label (A1-A10) and marked as R4 and R5 for Rails 4 and 5.