HexBrain

Ruby on Rails · Report #888176

HTTP Host injection in redirect_to function

Informativestatus
6upvotes
15 Jun 2021disclosed

How it was found

Hi team,

Here is the sample vulnerable code

class TesttestController < ApplicationController
  def index
    redirect_to  "/demo/?your_reset_token=your_reset_token"
  end
end

The ** _compute_redirect_to_location** will take the input "/demo/?your_reset_token=your_reset_token" as options variables. File File action_controller\metal\redirecting.rb line 63

    def redirect_to(options = {}, response_options = {})
      raise ActionControllerError.new("Cannot redirect to nil!") unless options
      raise AbstractController::DoubleRenderError if response_body

      self.status        = _extract_redirect_to_status(options, response_options)
      self.location      = _compute_redirect_to_location(request, options)
      self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
    end

Then it will check if the options, because the input is String, so it will be the concatenate of request.protocol + request.host_with_port + options File action_controller\metal\redirecting.rb line 96

    def _compute_redirect_to_location(request, options) #:nodoc:
      case options
      # The scheme name consist of a letter followed by any combination of
      # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
      # characters; and is terminated by a colon (":").
      # See https://tools.ietf.org/html/rfc3986#section-3.1
      # The protocol relative scheme starts with a double slash "//".
      when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
        options
      when String
        request.protocol + request.host_with_port + options
      when Proc
        _compute_redirect_to_location request, instance_eval(&options)
      else
        url_for(options)
      end.delete("\0\r\n")
    end
    module_function :_compute_redirect_to_location
    public :_compute_redirect_to_location

The request.protocol will be http:// or https:// The request.host_with_port will call raw_host_with_port to check if there is the X_FORWARD_FOR else, it will take the input from HTTP_HOST then continue the _compute_redirect_to_location process. file action_dispatch\http\url.rb line 220

      def raw_host_with_port
        if forwarded = x_forwarded_host.presence
          forwarded.split(/,\s?/).last
        else
          get_header("HTTP_HOST") || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"
        end
      end

So at the end, our input will be request.protocol + request.host_with_port + options and being set as Header["location"] value.

We cant change the HTTP_HOST to something else because the HTTP_HOST must be declare in the the config.host and raise 403 Status ( see the img4.JPG )

But if we use the IP, we can bypass this and here is the result, which 149.28.128.52 is the attacker IP, and there is no check for IP address but only the hostname.

The img2.JPG is the PoC request when use HTTP_HOST. The img3.JPG is the PoC request when use HTTP_X_FORWARD_HOST.

Fix: There should be a check for IP ADDRESS as well as the HOSTNAME in the enviroment config if user dont specific the host as parameter for redirect_to function.

Impact

Password Reset Poisoning - which user use the HTTP_HOST as the input for the reset token. Web-cache - which user use the HTTP_HOST as the input

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original