lib/brakeman/checks/check_redirect.rb
require 'brakeman/checks/base_check'
#Reports any calls to +redirect_to+ which include parameters in the arguments.
#
#For example:
#
# redirect_to params.merge(:action => :elsewhere)
class Brakeman::CheckRedirect < Brakeman::BaseCheck
Brakeman::Checks.add self
@description = "Looks for calls to redirect_to with user input as arguments"
def run_check
@model_find_calls = Set[:all, :create, :create!, :find, :find_by_sql, :first, :first!, :last, :last!, :new, :sole]
if tracker.options[:rails3]
@model_find_calls.merge [:from, :group, :having, :joins, :lock, :order, :reorder, :select, :where]
end
if version_between? "4.0.0", "9.9.9"
@model_find_calls.merge [:find_by, :find_by!, :take]
end
if version_between? "7.0.0", "9.9.9"
@model_find_calls << :find_sole_by
end
methods = [:redirect_to, :redirect_back, :redirect_back_or_to]
@tracker.find_call(:target => false, :methods => methods).each do |res|
process_result res
end
end
def process_result result
return unless original? result
call = result[:call]
opt = call.first_arg
# Location is specified with `fallback_location:`
# otherwise the arguments do not contain a location and
# the call can be ignored
if call.method == :redirect_back
if hash? opt and location = hash_access(opt, :fallback_location)
opt = location
else
return
end
end
if not protected_by_raise?(call) and
not only_path?(call) and
not explicit_host?(opt) and
not slice_call?(opt) and
not safe_permit?(opt) and
not disallow_other_host?(call) and
res = include_user_input?(opt)
if res.type == :immediate and not allow_other_host?(call)
confidence = :high
else
confidence = :weak
end
warn :result => result,
:warning_type => "Redirect",
:warning_code => :open_redirect,
:message => "Possible unprotected redirect",
:code => call,
:user_input => res,
:confidence => confidence,
:cwe_id => [601]
end
end
#Custom check for user input. First looks to see if the user input
#is being output directly. This is necessary because of tracker.options[:check_arguments]
#which can be used to enable/disable reporting output of method calls which use
#user input as arguments.
def include_user_input? opt, immediate = :immediate
Brakeman.debug "Checking if call includes user input"
# if the first argument is an array, rails assumes you are building a
# polymorphic route, which will never jump off-host
return false if array? opt
if tracker.options[:ignore_redirect_to_model]
if model_instance?(opt) or decorated_model?(opt)
return false
end
end
if res = has_immediate_model?(opt)
unless call? opt and opt.method.to_s =~ /_path/
return Match.new(immediate, res)
end
elsif call? opt
if request_value? opt
return Match.new(immediate, opt)
elsif opt.method == :url_for and include_user_input? opt.first_arg
return Match.new(immediate, opt)
#Ignore helpers like some_model_url?
elsif opt.method.to_s =~ /_(url|path)\z/
return false
elsif opt.method == :url_from
return false
end
elsif request_value? opt
return Match.new(immediate, opt)
elsif node_type? opt, :or
return (include_user_input?(opt.lhs) or include_user_input?(opt.rhs))
end
if tracker.options[:check_arguments] and call? opt
include_user_input? opt.first_arg, false #I'm doubting if this is really necessary...
else
false
end
end
#Checks +redirect_to+ arguments for +only_path => true+ which essentially
#nullifies the danger posed by redirecting with user input
def only_path? call
arg = call.first_arg
if hash? arg
return has_only_path? arg
elsif call? arg and arg.method == :url_for
return check_url_for(arg)
elsif call? arg and hash? arg.first_arg and use_unsafe_hash_method? arg
return has_only_path? arg.first_arg
end
false
end
def use_unsafe_hash_method? arg
return call_has_param(arg, :to_unsafe_hash) || call_has_param(arg, :to_unsafe_h)
end
def call_has_param arg, key
if call? arg and call? arg.target
target = arg.target
method = target.method
node_type? target.target, :params and method == key
else
false
end
end
def has_only_path? arg
if value = hash_access(arg, :only_path)
return true if true?(value)
end
false
end
def explicit_host? arg
return unless sexp? arg
if hash? arg
if value = hash_access(arg, :host)
return !has_immediate_user_input?(value)
end
elsif call? arg
target = arg.target
if hash? target and value = hash_access(target, :host)
return !has_immediate_user_input?(value)
elsif call? arg
return explicit_host? target
end
end
false
end
#+url_for+ is only_path => true by default. This checks to see if it is
#set to false for some reason.
def check_url_for call
arg = call.first_arg
if hash? arg
if value = hash_access(arg, :only_path)
return false if false?(value)
end
end
true
end
#Returns true if exp is (probably) a model instance
def model_instance? exp
if node_type? exp, :or
model_instance? exp.lhs or model_instance? exp.rhs
elsif call? exp
if model_target? exp and
(@model_find_calls.include? exp.method or exp.method.to_s.match(/^find_by_/))
true
else
association?(exp.target, exp.method)
end
end
end
def model_target? exp
return false unless call? exp
model_name? exp.target or
friendly_model? exp.target or
model_target? exp.target
end
#Returns true if exp is (probably) a friendly model instance
#using the FriendlyId gem
def friendly_model? exp
call? exp and model_name? exp.target and exp.method == :friendly
end
#Returns true if exp is (probably) a decorated model instance
#using the Draper gem
def decorated_model? exp
if node_type? exp, :or
decorated_model? exp.lhs or decorated_model? exp.rhs
else
tracker.config.has_gem? :draper and
call? exp and
node_type?(exp.target, :const) and
exp.target.value.to_s.match(/Decorator$/) and
exp.method == :decorate
end
end
#Check if method is actually an association in a Model
def association? model_name, meth
if call? model_name
return association? model_name.target, meth
elsif model_name? model_name
model = tracker.models[class_name(model_name)]
else
return false
end
return false unless model
model.association? meth
end
def slice_call? exp
return unless call? exp
exp.method == :slice
end
DANGEROUS_KEYS = [:host, :subdomain, :domain, :port]
def safe_permit? exp
if call? exp and params? exp.target and exp.method == :permit
exp.each_arg do |opt|
if symbol? opt and DANGEROUS_KEYS.include? opt.value
return false
end
end
return true
end
false
end
def protected_by_raise? call
raise_on_redirects? and
not allow_other_host? call
end
def raise_on_redirects?
@raise_on_redirects ||= true?(tracker.config.rails.dig(:action_controller, :raise_on_open_redirects))
end
def allow_other_host? call
opt = call.last_arg
hash? opt and true? hash_access(opt, :allow_other_host)
end
def disallow_other_host? call
opt = call.last_arg
hash? opt and false? hash_access(opt, :allow_other_host)
end
end