diff --git a/lib/brakeman/processors/alias_processor.rb b/lib/brakeman/processors/alias_processor.rb index b3305bb5b0..6207208b14 100644 --- a/lib/brakeman/processors/alias_processor.rb +++ b/lib/brakeman/processors/alias_processor.rb @@ -208,6 +208,15 @@ def process_call exp return Sexp.new(:false).line(exp.line) end + # For the simplest case of `Foo.thing` + if node_type? target, :const and first_arg.nil? + if tracker and (klass = tracker.find_class(class_name(target.value))) + if return_value = klass.get_simple_method_return_value(:class, method) + return return_value.deep_clone(exp.line) + end + end + end + #See if it is possible to simplify some basic cases #of addition/concatenation. case method @@ -794,6 +803,18 @@ def process_cdecl exp exp end + def hash_or_array_include_all_literals? exp + return unless call? exp and sexp? exp.target + target = exp.target + + case target.node_type + when :hash + hash_include_all_literals? exp + else + array_include_all_literals? exp + end + end + # Check if exp is a call to Array#include? on an array literal # that contains all literal values. For example: # @@ -812,6 +833,16 @@ def array_detect_all_literals? exp (all_literals? exp.target or dir_glob? exp.target) end + # Check if exp is a call to Hash#include? on a hash literal + # that contains all literal values. For example: + # + # {x: 1}.include? x + def hash_include_all_literals? exp + call? exp and + exp.method == :include? and + all_literals? exp.target, :hash + end + #Sets @inside_if = true def process_if exp if @ignore_ifs.nil? @@ -852,7 +883,7 @@ def process_if exp scope do @branch_env = env.current branch_index = 2 + i # s(:if, condition, then_branch, else_branch) - if i == 0 and array_include_all_literals? condition + if i == 0 and hash_or_array_include_all_literals? condition # If the condition is ["a", "b"].include? x # set x to "a" inside the true branch var = condition.first_arg @@ -860,7 +891,7 @@ def process_if exp env.current[var] = safe_literal(var.line) exp[branch_index] = process_if_branch branch env.current[var] = previous_value - elsif i == 1 and array_include_all_literals? condition and early_return? branch + elsif i == 1 and hash_or_array_include_all_literals? condition and early_return? branch var = condition.first_arg env.current[var] = safe_literal(var.line) exp[branch_index] = process_if_branch branch diff --git a/lib/brakeman/processors/lib/call_conversion_helper.rb b/lib/brakeman/processors/lib/call_conversion_helper.rb index efadb43226..59215976fd 100644 --- a/lib/brakeman/processors/lib/call_conversion_helper.rb +++ b/lib/brakeman/processors/lib/call_conversion_helper.rb @@ -1,11 +1,5 @@ module Brakeman module CallConversionHelper - def all_literals? exp, expected_type = :array - node_type? exp, expected_type and - exp.length > 1 and - exp.all? { |e| e.is_a? Symbol or node_type? e, :lit, :str } - end - # Join two array literals into one. def join_arrays lhs, rhs, original_exp = nil if array? lhs and array? rhs diff --git a/lib/brakeman/processors/model_processor.rb b/lib/brakeman/processors/model_processor.rb index 4fa517a2da..8a92187e00 100644 --- a/lib/brakeman/processors/model_processor.rb +++ b/lib/brakeman/processors/model_processor.rb @@ -73,6 +73,8 @@ def process_call exp @current_class.set_attr_accessible exp when :attr_protected @current_class.set_attr_protected exp + when :enum + add_enum_method exp else if @current_class @current_class.add_option method, exp @@ -87,4 +89,33 @@ def process_call exp call end end + + def add_enum_method call + arg = call.first_arg + return unless hash? arg + + enum_name = arg[1].value # first key + enums = arg[2] # first value + enums_name = pluralize(enum_name.to_s).to_sym + + call_line = call.line + + if hash? enums + enum_values = enums + elsif array? enums + # Build hash for enum values like Rails does + enum_values = s(:hash).line(call_line) + + enums.each_sexp.with_index do |v, index| + enum_values << v + enum_values << s(:lit, index).line(call_line) + end + end + + enum_method = s(:defn, enum_name, s(:args), safe_literal(call_line)) + enums_method = s(:defs, s(:self), enums_name, s(:args), enum_values) + + @current_class.add_method :public, enum_name, enum_method, @current_file + @current_class.add_method :public, enums_name, enums_method, @current_file + end end diff --git a/lib/brakeman/tracker/collection.rb b/lib/brakeman/tracker/collection.rb index 903f98187d..0255b1aee1 100644 --- a/lib/brakeman/tracker/collection.rb +++ b/lib/brakeman/tracker/collection.rb @@ -15,6 +15,7 @@ def initialize name, parent, file_name, src, tracker @includes = [] @methods = { :public => {}, :private => {}, :protected => {} } @class_methods = {} + @simple_methods = { :class => {}, instance: {} } @options = {} @tracker = tracker @@ -49,6 +50,7 @@ def add_option name, exp def add_method visibility, name, src, file_name meth_info = Brakeman::MethodInfo.new(name, src, self, file_name) + add_simple_method_maybe meth_info if src.node_type == :defs @class_methods[name] = meth_info @@ -112,5 +114,31 @@ def top_line def methods_public @methods[:public] end + + def get_simple_method_return_value type, name + @simple_methods[type][name] + end + + private + + def add_simple_method_maybe meth_info + if meth_info.very_simple_method? + add_simple_method meth_info + end + end + + def add_simple_method meth_info + name = meth_info.name + value = meth_info.return_value + + case meth_info.src.node_type + when :defn + @simple_methods[:instance][name] = value + when :defs + @simple_methods[:class][name] = value + else + raise "Expected sexp type: #{src.node_type}" + end + end end end diff --git a/lib/brakeman/tracker/method_info.rb b/lib/brakeman/tracker/method_info.rb index 6df344b8d5..31a2b6b4f4 100644 --- a/lib/brakeman/tracker/method_info.rb +++ b/lib/brakeman/tracker/method_info.rb @@ -19,11 +19,52 @@ def initialize name, src, owner, file else raise "Expected sexp type: #{src.node_type}" end + + @simple_method = nil end # To support legacy code that expected a Hash def [] attr self.send(attr) end + + def very_simple_method? + return @simple_method == :very unless @simple_method.nil? + + # Very simple methods have one (simple) expression in the body and + # no arguments + if src.formal_args.length == 1 # no args + if src.method_length == 1 # Single expression in body + value = first_body # First expression in body + + if simple_literal? value or + (array? value and all_literals? value) or + (hash? value and all_literals? value, :hash) + + @return_value = value + @simple_method = :very + end + end + end + + @simple_method ||= false + end + + def return_value env = nil + if very_simple_method? + return @return_value + else + nil + end + end + + def first_body + case @type + when :class + src[4] + when :instance + src[3] + end + end end end diff --git a/lib/brakeman/util.rb b/lib/brakeman/util.rb index 9507003e52..78dfb1029b 100644 --- a/lib/brakeman/util.rb +++ b/lib/brakeman/util.rb @@ -50,7 +50,11 @@ def underscore camel_cased_word # stupid simple, used to delegate to ActiveSupport def pluralize word - word + "s" + if word.end_with? 's' + word + 'es' + else + word + 's' + end end #Returns a class name as a Symbol. @@ -293,12 +297,24 @@ def node_type? exp, *types exp.is_a? Sexp and types.include? exp.node_type end - LITERALS = [:lit, :false, :str, :true, :array, :hash] + SIMPLE_LITERALS = [:lit, :false, :str, :true] + + def simple_literal? exp + exp.is_a? Sexp and SIMPLE_LITERALS.include? exp.node_type + end + + LITERALS = [*SIMPLE_LITERALS, :array, :hash] def literal? exp exp.is_a? Sexp and LITERALS.include? exp.node_type end + def all_literals? exp, expected_type = :array + node_type? exp, expected_type and + exp.length > 1 and + exp.all? { |e| e.is_a? Symbol or node_type? e, :lit, :str } + end + DIR_CONST = s(:const, :Dir) # Dir.glob(...).whatever diff --git a/lib/ruby_parser/bm_sexp.rb b/lib/ruby_parser/bm_sexp.rb index abc6f6f007..3cfe7dd30e 100644 --- a/lib/ruby_parser/bm_sexp.rb +++ b/lib/ruby_parser/bm_sexp.rb @@ -543,6 +543,20 @@ def body_list self.body.unshift :rlist end + # Number of "statements" in a method. + # This is more effecient than `Sexp#body.length` + # because `Sexp#body` creates a new Sexp. + def method_length + expect :defn, :defs + + case self.node_type + when :defn + self.length - 3 + when :defs + self.length - 4 + end + end + def render_type expect :render self[1] diff --git a/test/apps/rails6/app/controllers/groups_controller.rb b/test/apps/rails6/app/controllers/groups_controller.rb index 2510140123..4468113ea8 100644 --- a/test/apps/rails6/app/controllers/groups_controller.rb +++ b/test/apps/rails6/app/controllers/groups_controller.rb @@ -69,4 +69,15 @@ def test_rails6_sqli User.order("name #{params[:direction]}") # Warn in 6.0, not in 6.1 User.order(:name).reorder(params[:column]) # Warn in 6.0, not in 6.1 end + + # From https://github.com/presidentbeef/brakeman/issues/1492 + def enum_include_check + status = "#{params[:status]}" + if Group.statuses.include? status + @status = status.to_sym + @countries = Group.send(@status) # Should not warn + else + redirect_to root_path, notice: 'Invalid status' + end + end end diff --git a/test/apps/rails6/app/models/group.rb b/test/apps/rails6/app/models/group.rb index a2fc777764..0ceba8ec68 100644 --- a/test/apps/rails6/app/models/group.rb +++ b/test/apps/rails6/app/models/group.rb @@ -18,4 +18,20 @@ def fetch_constant_hash_value(role_name) role = roles.fetch(role_name) Arel.sql("role = '#{role}'") end + + def use_simple_method + # No warning + self.where("thing = #{Group.simple_method}") + end + + def self.simple_method + "Hello" + end + + enum status: { start: 0, stop: 2, in_process: 3 } + + def use_enum + # No warning + self.where("thing IN #{Group.statuses.values_at(*[:start, :stop]).join(',')}") + end end diff --git a/test/apps/rails6/app/models/user.rb b/test/apps/rails6/app/models/user.rb index f339f4f2b0..24c890c333 100644 --- a/test/apps/rails6/app/models/user.rb +++ b/test/apps/rails6/app/models/user.rb @@ -24,4 +24,10 @@ def self.more_heredocs def recent_stuff where("date > #{Date.today - 1}") end + + enum state: ["pending", "active", "archived"] + + def check_enum + where("state = #{User.states["pending"]}") + end end diff --git a/test/tests/rails6.rb b/test/tests/rails6.rb index 3323cd16e0..d9fa71d263 100644 --- a/test/tests/rails6.rb +++ b/test/tests/rails6.rb @@ -254,6 +254,32 @@ def test_sql_injection_reorder :user_input => s(:call, s(:params), :[], s(:lit, :column)) end + def test_sql_injection_enum + assert_no_warning :type => :warning, + :warning_code => 0, + :fingerprint => "b2071137eba7ef6ecbcc1c6381a428e5c576a5fadf73dc04b2e155c41043e1d2", + :warning_type => "SQL Injection", + :line => 31, + :message => /^Possible\ SQL\ injection/, + :confidence => 0, + :relative_path => "app/models/user.rb", + :code => s(:call, nil, :where, s(:dstr, "state = ", s(:evstr, s(:call, s(:call, s(:const, :User), :states), :[], s(:str, "pending"))))), + :user_input => s(:call, s(:call, s(:const, :User), :states), :[], s(:str, "pending")) + end + + def test_dangerous_send_enum + assert_no_warning :type => :warning, + :warning_code => 23, + :fingerprint => "483fa36e41f5791e86f345a19b517a61859886d685ce40ef852871bb7a935f2d", + :warning_type => "Dangerous Send", + :line => 78, + :message => /^User\ controlled\ method\ execution/, + :confidence => 0, + :relative_path => "app/controllers/groups_controller.rb", + :code => s(:call, s(:const, :Group), :send, s(:call, s(:dstr, "", s(:evstr, s(:call, s(:params), :[], s(:lit, :status)))), :to_sym)), + :user_input => s(:call, s(:params), :[], s(:lit, :status)) + end + def test_cross_site_scripting_sanity assert_warning :type => :template, :warning_code => 2,