# frozen-string-literal: true # class Roda module RodaPlugins # The host_authorization plugin allows configuring an authorized host or # an array of authorized hosts. Then in the routing tree, you can check # whether the request uses an authorized host via the +check_host_authorized!+ # method. # # If the request doesn't match one of the authorized hosts, the # request processing stops at that point. Using this plugin can prevent # DNS rebinding attacks if the application can receive requests for # arbitrary hosts. # # By default, an empty response using status 403 will be returned for requests # with unauthorized hosts. # # Because +check_host_authorized!+ is an instance method, you can easily choose # to only check for authorization in certain routes, or to check it after # other processing. For example, you could check for authorized hosts after # serving static files, since the serving of static files should not be # vulnerable to DNS rebinding attacks. # # = Usage # # In your routing tree, call the +check_host_authorized!+ method at the point you # want to check for authorized hosts: # # plugin :host_authorization, 'www.example.com' # plugin :public # # route do |r| # r.public # check_host_authorized! # # # ... # end # # = Specifying authorized hosts # # For applications hosted on a single domain name, you can use a single string: # # plugin :host_authorization, 'www.example.com' # # For applications hosted on multiple domain names, you can use an array of strings: # # plugin :host_authorization, %w'www.example.com www.example2.com' # # For applications supporting arbitrary subdomains, you can use a regexp. If using # a regexp, make sure you use \A and \z in your regexp, and restrict # the allowed characters to the minimum required, otherwise you can potentionally # introduce a security issue: # # plugin :host_authorization, /\A[-0-9a-f]+\.example\.com\z/ # # For applications with more complex requirements, you can use a proc. Similarly # to the regexp case, the proc should be aware the host contains user-submitted # values, and not assume it is in any particular format: # # plugin :host_authorization, proc{|host| ExternalService.allowed_host?(host)} # # If an array of values is passed as the host argument, the host is authorized if # it matches any value in the array. All host authorization checks use the # === method, which is why it works for strings, regexps, and procs. # It can also work with arbitrary objects that support ===. # # For security reasons, only the +Host+ header is checked by default. If you are # sure that your application is being run behind a forwarding proxy that sets the # X-Forwarded-Host header, you should enable support for checking that # header using the +:check_forwarded+ option: # # plugin :host_authorization, 'www.example.com', check_forwarded: true # # In this case, the trailing host in the X-Forwarded-Host header is checked, # which should be the host set by the forwarding proxy closest to the application. # In cases where multiple forwarding proxies are used that append to the # X-Forwarded-Host header, you should not use this plugin. # # = Customizing behavior # # By default, an unauthorized host will receive an empty 403 response. You can # customize this by passing a block when loading the plugin. For example, for # sites using the render plugin, you could return a page that uses your default # layout: # # plugin :render # plugin :host_authorization, 'www.example.com' do |r| # response.status = 403 # view(:content=>"

Forbidden

") # end # # The block passed to this plugin is treated as a match block. module HostAuthorization def self.configure(app, host, opts=OPTS, &block) app.opts[:host_authorization_host] = host app.opts[:host_authorization_check_forwarded] = opts[:check_forwarded] if opts.key?(:check_forwarded) if block app.define_roda_method(:host_authorization_unauthorized, 1, &block) end end module InstanceMethods # Check whether the host is authorized. If not authorized, return a response # immediately based on the plugin block. def check_host_authorization! r = @_request return if host_authorized?(_convert_host_for_authorization(r.env["HTTP_HOST"].to_s.dup)) if opts[:host_authorization_check_forwarded] && (host = r.env["HTTP_X_FORWARDED_HOST"]) if i = host.rindex(',') host = host[i+1, 10000000].to_s end host = _convert_host_for_authorization(host.strip) if !host.empty? && host_authorized?(host) return end end r.on do host_authorization_unauthorized(r) end end private # Remove the port information from the passed string (mutates the passed argument). def _convert_host_for_authorization(host) host.sub!(/:\d+\z/, "") host end # Whether the host given is one of the authorized hosts for this application. def host_authorized?(host, authorized_host = opts[:host_authorization_host]) case authorized_host when Array authorized_host.any?{|auth_host| host_authorized?(host, auth_host)} else authorized_host === host end end # Action to take for unauthorized hosts. Sets a 403 status by default. def host_authorization_unauthorized(_) @_response.status = 403 nil end end end register_plugin(:host_authorization, HostAuthorization) end end