class Brakeman::BaseCheck

Basis of vulnerability checks.

Constants

CONFIDENCE

This is for legacy support. Use :high, :medium, or :low instead when creating warnings.

I18N_CLASS
Match
STRING_METHODS
TEMP_FILE_PATH

Attributes

name[RW]
tracker[R]
warnings[R]

Public Class Methods

description() click to toggle source
# File lib/brakeman/checks/base_check.rb, line 486
def self.description
  @description
end
inherited(subclass) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 24
def inherited(subclass)
  subclass.name = subclass.to_s.match(/^Brakeman::(.*)$/)[1]
end
new(tracker) click to toggle source

Initialize Check with Checks.

Calls superclass method Brakeman::SexpProcessor::new
# File lib/brakeman/checks/base_check.rb, line 30
def initialize(tracker)
  super()
  @app_tree = tracker.app_tree
  @results = [] #only to check for duplicates
  @warnings = []
  @tracker = tracker
  @string_interp = false
  @current_set = nil
  @current_template = @current_module = @current_class = @current_method = nil
  @active_record_models = nil
  @mass_assign_disabled = nil
  @has_user_input = nil
  @in_array = false
  @safe_input_attributes = Set[:to_i, :to_f, :arel_table, :id, :uuid]
  @comparison_ops  = Set[:==, :!=, :>, :<, :>=, :<=]
end

Public Instance Methods

add_result(result) click to toggle source

Add result to result list, which is used to check for duplicates

# File lib/brakeman/checks/base_check.rb, line 48
def add_result result
  location = get_location result
  location, line = get_location result

  @results << [line, location, result]
end
process_array(exp) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 113
def process_array exp
  @in_array = true
  process_default exp
ensure
  @in_array = false
end
process_call(exp) click to toggle source

Process calls and check if they include user input

# File lib/brakeman/checks/base_check.rb, line 66
def process_call exp
  unless @comparison_ops.include? exp.method
    process exp.target if sexp? exp.target
    process_call_args exp
  end

  target = exp.target

  unless always_safe_method? exp.method
    if params? target
      @has_user_input = Match.new(:params, exp)
    elsif cookies? target
      @has_user_input = Match.new(:cookies, exp)
    elsif request_headers? target
      @has_user_input = Match.new(:request, exp)
    elsif sexp? target and model_name? target[1] #TODO: Can this be target.target?
      @has_user_input = Match.new(:model, exp)
    end
  end

  exp
end
process_cookies(exp) click to toggle source

Note that cookies are included in current expression

# File lib/brakeman/checks/base_check.rb, line 108
def process_cookies exp
  @has_user_input = Match.new(:cookies, exp)
  exp
end
process_default(exp) click to toggle source

Default Sexp processing. Iterates over each value in the Sexp and processes them if they are also Sexps.

# File lib/brakeman/checks/base_check.rb, line 57
def process_default exp
  exp.each do |e|
    process e if sexp? e
  end

  exp
end
process_dstr(exp) click to toggle source

Does not actually process string interpolation, but notes that it occurred.

# File lib/brakeman/checks/base_check.rb, line 121
def process_dstr exp
  unless array_interp? exp or @string_interp # don't overwrite existing value
    @string_interp = Match.new(:interp, exp)
  end

  process_default exp
end
process_if(exp) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 89
def process_if exp
  #This is to ignore user input in condition
  current_user_input = @has_user_input
  process exp.condition
  @has_user_input = current_user_input

  process exp.then_clause if sexp? exp.then_clause
  process exp.else_clause if sexp? exp.else_clause

  exp
end
process_params(exp) click to toggle source

Note that params are included in current expression

# File lib/brakeman/checks/base_check.rb, line 102
def process_params exp
  @has_user_input = Match.new(:params, exp)
  exp
end

Private Instance Methods

active_record_models() click to toggle source
# File lib/brakeman/checks/base_check.rb, line 490
def active_record_models
  return @active_record_models if @active_record_models

  @active_record_models = {}

  tracker.models.each do |name, model|
    if model.ancestor? :"ActiveRecord::Base"
      @active_record_models[name] = model
    end
  end

  @active_record_models
end
always_safe_method?(meth) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 145
def always_safe_method? meth
  @safe_input_attributes.include? meth or
    @comparison_ops.include? meth
end
array_interp?(exp) click to toggle source

Checking for

%W[#{a}]

which will be parsed as

s(:array, s(:dstr, "", s(:evstr, s(:call, nil, :a))))
# File lib/brakeman/checks/base_check.rb, line 138
def array_interp? exp
  @in_array and
    string_interp? exp and
    exp[1] == "".freeze and
    exp.length == 3 # only one interpolated value
end
boolean_method?(method) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 150
def boolean_method? method
  method[-1] == "?"
end
duplicate?(result, location = nil) click to toggle source

This is to avoid reporting duplicates. Checks if the result has been reported already from the same line number.

# File lib/brakeman/checks/base_check.rb, line 255
def duplicate? result, location = nil
  location, line = get_location result

  @results.each do |r|
    if r[0] == line and r[1] == location
      if tracker.options[:combine_locations]
        return true
      elsif r[2] == result
        return true
      end
    end
  end

  false
end
format_output(exp) click to toggle source

Run exp through OutputProcessor to get a nice String.

# File lib/brakeman/checks/base_check.rb, line 172
def format_output exp
  Brakeman::OutputProcessor.new.format(exp).gsub(/\r|\n/, "")
end
gemfile_or_environment(gem_name = :rails) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 474
def gemfile_or_environment gem_name = :rails
  if gem_name and info = tracker.config.get_gem(gem_name.to_sym)
    info
  elsif @app_tree.exists?("Gemfile")
    @app_tree.file_path "Gemfile"
  elsif @app_tree.exists?("gems.rb")
    @app_tree.file_path "gems.rb"
  else
    @app_tree.file_path "config/environment.rb"
  end
end
get_location(result) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 271
def get_location result
  if result.is_a? Hash
    line = result[:call].original_line || result[:call].line
  elsif sexp? result
    line = result.original_line || result.line
  else
    raise ArgumentError
  end

  location ||= (@current_template && @current_template.name) || @current_class || @current_module || @current_set || result[:location][:class] || result[:location][:template] || result[:location][:file].to_s

  location = location[:name] if location.is_a? Hash
  location = location.name if location.is_a? Brakeman::Collection
  location = location.to_sym

  return location, line
end
has_immediate_model?(exp, out = nil) click to toggle source

Checks for a model attribute at the top level of the expression.

# File lib/brakeman/checks/base_check.rb, line 375
def has_immediate_model? exp, out = nil
  out = exp if out.nil?

  if sexp? exp and exp.node_type == :output
    exp = exp.value
  end

  if call? exp
    target = exp.target
    method = exp.method

    if always_safe_method? method
      false
    elsif call? target and not method.to_s[-1,1] == "?"
      if has_immediate_model?(target, out)
        exp
      else
        false
      end
    elsif model_name? target
      exp
    else
      false
    end
  elsif sexp? exp
    case exp.node_type
    when :dstr
      exp.each do |e|
        if sexp? e and match = has_immediate_model?(e, out)
          return match
        end
      end
      false
    when :evstr
      if sexp? exp.value
        if exp.value.node_type == :rlist
          exp.value.each_sexp do |e|
            if match = has_immediate_model?(e, out)
              return match
            end
          end
          false
        else
          has_immediate_model? exp.value, out
        end
      end
    when :format
      has_immediate_model? exp.value, out
    when :if
      ((sexp? exp.then_clause and has_immediate_model? exp.then_clause, out) or
       (sexp? exp.else_clause and has_immediate_model? exp.else_clause, out))
    when :or
      has_immediate_model? exp.lhs or
      has_immediate_model? exp.rhs
    else
      false
    end
  end
end
has_immediate_user_input?(exp) click to toggle source

This is used to check for user input being used directly.

If found, returns a struct containing a type (:cookies, :params, :request) and

the matching expression (Match#type and Match#match).

Returns false otherwise.

# File lib/brakeman/checks/base_check.rb, line 308
def has_immediate_user_input? exp
  if exp.nil?
    false
  elsif call? exp and not always_safe_method? exp.method
    if params? exp
      return Match.new(:params, exp)
    elsif cookies? exp
      return Match.new(:cookies, exp)
    elsif request_headers? exp
      return Match.new(:request, exp)
    else
      has_immediate_user_input? exp.target
    end
  elsif sexp? exp
    case exp.node_type
    when :dstr
      exp.each do |e|
        if sexp? e
          match = has_immediate_user_input?(e)
          return match if match
        end
      end
      false
    when :evstr
      if sexp? exp.value
        if exp.value.node_type == :rlist
          exp.value.each_sexp do |e|
            match = has_immediate_user_input?(e)
            return match if match
          end
          false
        else
          has_immediate_user_input? exp.value
        end
      end
    when :format
      has_immediate_user_input? exp.value
    when :if
      (sexp? exp.then_clause and has_immediate_user_input? exp.then_clause) or
      (sexp? exp.else_clause and has_immediate_user_input? exp.else_clause)
    when :or
      has_immediate_user_input? exp.lhs or
      has_immediate_user_input? exp.rhs
    when :splat, :kwsplat
      exp.each_sexp do |e|
        match = has_immediate_user_input?(e)
        return match if match
      end

      false
    when :hash
      if kwsplat? exp
        exp[1].each_sexp do |e|
          match = has_immediate_user_input?(e)
          return match if match
        end

        false
      end
    else
      false
    end
  end
end
include_target?(exp, target) click to toggle source

Returns true if target is in exp

# File lib/brakeman/checks/base_check.rb, line 455
def include_target? exp, target
  return false unless call? exp

  exp.each do |e|
    return true if e == target or include_target? e, target
  end

  false
end
include_user_input?(exp) click to toggle source

Checks if exp includes user input in the form of cookies, parameters, request environment, or model attributes.

If found, returns a struct containing a type (:cookies, :params, :request, :model) and the matching expression (Match#type and Match#match).

Returns false otherwise.

# File lib/brakeman/checks/base_check.rb, line 296
def include_user_input? exp
  @has_user_input = false
  process exp
  @has_user_input
end
locale_call?(exp) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 518
def locale_call? exp
  return unless call? exp

  (exp.target == I18N_CLASS and
   exp.method == :locale) or
  locale_call? exp.target
end
lts_version?(version) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 465
def lts_version? version
  tracker.config.has_gem? :'railslts-version' and
  version_between? version, "2.3.18.99", tracker.config.gem_version(:'railslts-version')
end
mass_assign_disabled?() click to toggle source

Checks if mass assignment is disabled globally in an initializer.

# File lib/brakeman/checks/base_check.rb, line 177
def mass_assign_disabled?
  return @mass_assign_disabled unless @mass_assign_disabled.nil?

  @mass_assign_disabled = false

  if version_between?("3.1.0", "3.9.9") and
    tracker.config.whitelist_attributes?

    @mass_assign_disabled = true
  elsif tracker.options[:rails4] && (!tracker.config.has_gem?(:protected_attributes) || tracker.config.whitelist_attributes?)

    @mass_assign_disabled = true
  else
    #Check for ActiveRecord::Base.send(:attr_accessible, nil)
    tracker.find_call(target: :"ActiveRecord::Base", method: :attr_accessible).each do |result|
      call = result[:call]

      if call? call
        if call.first_arg == Sexp.new(:nil)
          @mass_assign_disabled = true
          break
        end
      end
    end

    unless @mass_assign_disabled
      #Check for
      #  class ActiveRecord::Base
      #    attr_accessible nil
      #  end
      tracker.check_initializers([], :attr_accessible).each do |result|
        if result.module == "ActiveRecord" and result.result_class == :Base
          arg = result.call.first_arg

          if arg.nil? or node_type? arg, :nil
            @mass_assign_disabled = true
            break
          end
        end
      end
    end
  end

  #There is a chance someone is using Rails 3.x and the `strong_parameters`
  #gem and still using hack above, so this is a separate check for
  #including ActiveModel::ForbiddenAttributesProtection in
  #ActiveRecord::Base in an initializer.
  if not @mass_assign_disabled and version_between?("3.1.0", "3.9.9") and tracker.config.has_gem? :strong_parameters
    matches = tracker.check_initializers([], :include)
    forbidden_protection = Sexp.new(:colon2, Sexp.new(:const, :ActiveModel), :ForbiddenAttributesProtection)

    matches.each do |result|
      if call? result.call and result.call.first_arg == forbidden_protection
        @mass_assign_disabled = true
      end
    end

    unless @mass_assign_disabled
      tracker.find_call(target: :"ActiveRecord::Base", method: [:send, :include]).each do |result|
        call = result[:call]
        if call? call and (call.first_arg == forbidden_protection or call.second_arg == forbidden_protection)
          @mass_assign_disabled = true
        end
      end
    end
  end

  @mass_assign_disabled
end
model_name?(exp) click to toggle source

Checks if exp is a model name.

Prior to using this method, either @tracker must be set to the current tracker, or else @models should contain an array of the model names, which is available via tracker.models.keys

# File lib/brakeman/checks/base_check.rb, line 440
def model_name? exp
  @models ||= @tracker.models.keys

  if exp.is_a? Symbol
    @models.include? exp
  elsif call? exp and exp.target.nil? and exp.method == :current_user
    true
  elsif sexp? exp
    @models.include? class_name(exp)
  else
    false
  end
end
original?(result) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 247
def original? result
  return false if result[:call].original_line or duplicate? result
  add_result result
  true
end
string_building?(exp) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 507
def string_building? exp
  return false unless call? exp and STRING_METHODS.include? exp.method

  node_type? exp.target, :str, :dstr or
  node_type? exp.first_arg, :str, :dstr or
  string_building? exp.target or
  string_building? exp.first_arg
end
temp_file_path?(exp) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 156
def temp_file_path? exp
  exp == TEMP_FILE_PATH
end
version_between?(low_version, high_version, current_version = nil) click to toggle source
# File lib/brakeman/checks/base_check.rb, line 470
def version_between? low_version, high_version, current_version = nil
  tracker.config.version_between? low_version, high_version, current_version
end
warn(options) click to toggle source

Report a warning

# File lib/brakeman/checks/base_check.rb, line 161
def warn options
  extra_opts = { :check => self.class.to_s }

  if options[:file]
    options[:file] = @app_tree.file_path(options[:file])
  end

  @warnings << Brakeman::Warning.new(options.merge(extra_opts))
end