Skip to content

Commit

Permalink
Merge pull request #1618 from presidentbeef/support_ar_enums
Browse files Browse the repository at this point in the history
Initial support for ActiveRecord enums (and other things, too)
  • Loading branch information
presidentbeef committed Jul 16, 2021
2 parents b1b7075 + 1920a23 commit e658d78
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 10 deletions.
35 changes: 33 additions & 2 deletions lib/brakeman/processors/alias_processor.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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:
#
Expand All @@ -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?
Expand Down Expand Up @@ -852,15 +883,15 @@ 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
previous_value = env.current[var]
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
Expand Down
6 changes: 0 additions & 6 deletions 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
Expand Down
31 changes: 31 additions & 0 deletions lib/brakeman/processors/model_processor.rb
Expand Up @@ -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
Expand All @@ -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
28 changes: 28 additions & 0 deletions lib/brakeman/tracker/collection.rb
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
41 changes: 41 additions & 0 deletions lib/brakeman/tracker/method_info.rb
Expand Up @@ -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
20 changes: 18 additions & 2 deletions lib/brakeman/util.rb
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions lib/ruby_parser/bm_sexp.rb
Expand Up @@ -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]
Expand Down
11 changes: 11 additions & 0 deletions test/apps/rails6/app/controllers/groups_controller.rb
Expand Up @@ -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
16 changes: 16 additions & 0 deletions test/apps/rails6/app/models/group.rb
Expand Up @@ -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
6 changes: 6 additions & 0 deletions test/apps/rails6/app/models/user.rb
Expand Up @@ -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
26 changes: 26 additions & 0 deletions test/tests/rails6.rb
Expand Up @@ -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,
Expand Down

0 comments on commit e658d78

Please sign in to comment.