brakeman-lib 4.10.1 → 5.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +44 -0
  3. data/README.md +11 -2
  4. data/lib/brakeman.rb +17 -4
  5. data/lib/brakeman/app_tree.rb +36 -3
  6. data/lib/brakeman/checks/base_check.rb +7 -1
  7. data/lib/brakeman/checks/check_execute.rb +1 -0
  8. data/lib/brakeman/checks/check_mass_assignment.rb +4 -6
  9. data/lib/brakeman/checks/check_sanitize_methods.rb +2 -1
  10. data/lib/brakeman/checks/check_sql.rb +1 -1
  11. data/lib/brakeman/checks/check_unsafe_reflection_methods.rb +68 -0
  12. data/lib/brakeman/checks/check_verb_confusion.rb +75 -0
  13. data/lib/brakeman/file_parser.rb +19 -18
  14. data/lib/brakeman/options.rb +5 -1
  15. data/lib/brakeman/parsers/template_parser.rb +26 -3
  16. data/lib/brakeman/processors/alias_processor.rb +39 -12
  17. data/lib/brakeman/processors/base_processor.rb +4 -4
  18. data/lib/brakeman/processors/lib/file_type_detector.rb +64 -0
  19. data/lib/brakeman/processors/lib/rails3_config_processor.rb +16 -16
  20. data/lib/brakeman/processors/lib/rails4_config_processor.rb +2 -1
  21. data/lib/brakeman/report.rb +8 -0
  22. data/lib/brakeman/report/report_base.rb +0 -2
  23. data/lib/brakeman/report/report_csv.rb +37 -60
  24. data/lib/brakeman/report/report_junit.rb +2 -2
  25. data/lib/brakeman/report/report_sarif.rb +1 -1
  26. data/lib/brakeman/report/report_sonar.rb +38 -0
  27. data/lib/brakeman/report/report_tabs.rb +1 -1
  28. data/lib/brakeman/report/report_text.rb +1 -1
  29. data/lib/brakeman/rescanner.rb +7 -5
  30. data/lib/brakeman/scanner.rb +44 -18
  31. data/lib/brakeman/tracker.rb +6 -0
  32. data/lib/brakeman/tracker/config.rb +73 -0
  33. data/lib/brakeman/util.rb +7 -2
  34. data/lib/brakeman/version.rb +1 -1
  35. data/lib/brakeman/warning.rb +10 -2
  36. data/lib/brakeman/warning_codes.rb +2 -0
  37. metadata +8 -4
@@ -3,32 +3,31 @@ module Brakeman
3
3
 
4
4
  # This class handles reading and parsing files.
5
5
  class FileParser
6
- attr_reader :file_list
6
+ attr_reader :file_list, :errors
7
7
 
8
- def initialize tracker
9
- @tracker = tracker
10
- @timeout = @tracker.options[:parser_timeout]
11
- @app_tree = @tracker.app_tree
12
- @file_list = {}
8
+ def initialize app_tree, timeout
9
+ @app_tree = app_tree
10
+ @timeout = timeout
11
+ @file_list = []
12
+ @errors = []
13
13
  end
14
14
 
15
- def parse_files list, type
16
- read_files list, type do |path, contents|
15
+ def parse_files list
16
+ read_files list do |path, contents|
17
17
  if ast = parse_ruby(contents, path.relative)
18
18
  ASTFile.new(path, ast)
19
19
  end
20
20
  end
21
21
  end
22
22
 
23
- def read_files list, type
24
- @file_list[type] ||= []
25
-
23
+ def read_files list
26
24
  list.each do |path|
27
25
  file = @app_tree.file_path(path)
28
26
 
29
27
  result = yield file, file.read
28
+
30
29
  if result
31
- @file_list[type] << result
30
+ @file_list << result
32
31
  end
33
32
  end
34
33
  end
@@ -43,15 +42,17 @@ module Brakeman
43
42
  Brakeman.debug "Parsing #{path}"
44
43
  RubyParser.new.parse input, path, @timeout
45
44
  rescue Racc::ParseError => e
46
- @tracker.error e, "Could not parse #{path}"
47
- nil
45
+ error e.exception(e.message + "\nCould not parse #{path}")
48
46
  rescue Timeout::Error => e
49
- @tracker.error Exception.new("Parsing #{path} took too long (> #{@timeout} seconds). Try increasing the limit with --parser-timeout"), caller
50
- nil
47
+ error Exception.new("Parsing #{path} took too long (> #{@timeout} seconds). Try increasing the limit with --parser-timeout")
51
48
  rescue => e
52
- @tracker.error e.exception(e.message + "\nWhile processing #{path}"), e.backtrace
53
- nil
49
+ error e.exception(e.message + "\nWhile processing #{path}")
54
50
  end
55
51
  end
52
+
53
+ def error exception
54
+ @errors << exception
55
+ nil
56
+ end
56
57
  end
57
58
  end
@@ -166,6 +166,10 @@ module Brakeman::Options
166
166
  options[:only_files].merge files
167
167
  end
168
168
 
169
+ opts.on "--[no-]skip-vendor", "Skip processing vendor directory (Default)" do |skip|
170
+ options[:skip_vendor] = skip
171
+ end
172
+
169
173
  opts.on "--skip-libs", "Skip processing lib directory" do
170
174
  options[:skip_libs] = true
171
175
  end
@@ -229,7 +233,7 @@ module Brakeman::Options
229
233
 
230
234
  opts.on "-f",
231
235
  "--format TYPE",
232
- [:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc, :plain, :table, :junit, :sarif],
236
+ [:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc, :plain, :table, :junit, :sarif, :sonar],
233
237
  "Specify output formats. Default is text" do |type|
234
238
 
235
239
  type = "s" if type == :text
@@ -9,7 +9,7 @@ module Brakeman
9
9
  def initialize tracker, file_parser
10
10
  @tracker = tracker
11
11
  @file_parser = file_parser
12
- @file_parser.file_list[:templates] ||= []
12
+ @slim_smart = nil # Load slim/smart ?
13
13
  end
14
14
 
15
15
  def parse_template path, text
@@ -33,7 +33,7 @@ module Brakeman
33
33
  end
34
34
 
35
35
  if src and ast = @file_parser.parse_ruby(src, path)
36
- @file_parser.file_list[:templates] << TemplateFile.new(path, ast, name, type)
36
+ @file_parser.file_list << TemplateFile.new(path, ast, name, type)
37
37
  end
38
38
  rescue Racc::ParseError => e
39
39
  tracker.error e, "Could not parse #{path}"
@@ -89,6 +89,14 @@ module Brakeman
89
89
 
90
90
  def parse_slim path, text
91
91
  Brakeman.load_brakeman_dependency 'slim'
92
+
93
+ if @slim_smart.nil? and load_slim_smart?
94
+ @slim_smart = true
95
+ Brakeman.load_brakeman_dependency 'slim/smart'
96
+ else
97
+ @slim_smart = false
98
+ end
99
+
92
100
  require_relative 'slim_embedded'
93
101
 
94
102
  Slim::Template.new(path,
@@ -96,8 +104,23 @@ module Brakeman
96
104
  :generator => Temple::Generators::RailsOutputBuffer) { text }.precompiled_template
97
105
  end
98
106
 
107
+ def load_slim_smart?
108
+ return !@slim_smart unless @slim_smart.nil?
109
+
110
+ # Terrible hack to find
111
+ # gem "slim", "~> 3.0.1", require: ["slim", "slim/smart"]
112
+ if tracker.app_tree.exists? 'Gemfile'
113
+ gemfile_contents = tracker.app_tree.file_path('Gemfile').read
114
+ if gemfile_contents.include? 'slim/smart'
115
+ return true
116
+ end
117
+ end
118
+
119
+ false
120
+ end
121
+
99
122
  def self.parse_inline_erb tracker, text
100
- fp = Brakeman::FileParser.new(tracker)
123
+ fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout])
101
124
  tp = self.new(tracker, fp)
102
125
  src = tp.parse_erb '_inline_', text
103
126
  type = tp.erubis? ? :erubis : :erb
@@ -161,6 +161,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
161
161
  ARRAY_CONST = s(:const, :Array)
162
162
  HASH_CONST = s(:const, :Hash)
163
163
  RAILS_TEST = s(:call, s(:call, s(:const, :Rails), :env), :test?)
164
+ RAILS_DEV = s(:call, s(:call, s(:const, :Rails), :env), :development?)
164
165
 
165
166
  #Process a method call.
166
167
  def process_call exp
@@ -182,11 +183,17 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
182
183
  return exp
183
184
  end
184
185
 
186
+ # If x(*[1,2,3]) change to x(1,2,3)
187
+ # if that's the only argument
188
+ if splat_array? exp.first_arg and exp.second_arg.nil?
189
+ exp.arglist = exp.first_arg[1].sexp_body
190
+ end
191
+
185
192
  target = exp.target
186
193
  method = exp.method
187
194
  first_arg = exp.first_arg
188
195
 
189
- if method == :send or method == :try
196
+ if method == :send or method == :__send__ or method == :try
190
197
  collapse_send_call exp, first_arg
191
198
  end
192
199
 
@@ -194,11 +201,11 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
194
201
  res = process_or_simple_operation(exp)
195
202
  return res if res
196
203
  elsif target == ARRAY_CONST and method == :new
197
- return Sexp.new(:array, *exp.args)
204
+ return Sexp.new(:array, *exp.args).line(exp.line)
198
205
  elsif target == HASH_CONST and method == :new and first_arg.nil? and !node_type?(@exp_context.last, :iter)
199
- return Sexp.new(:hash)
200
- elsif exp == RAILS_TEST
201
- return Sexp.new(:false)
206
+ return Sexp.new(:hash).line(exp.line)
207
+ elsif exp == RAILS_TEST or exp == RAILS_DEV
208
+ return Sexp.new(:false).line(exp.line)
202
209
  end
203
210
 
204
211
  #See if it is possible to simplify some basic cases
@@ -236,7 +243,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
236
243
  env[target_var] = target
237
244
  return target
238
245
  elsif string? target and string_interp? first_arg
239
- exp = Sexp.new(:dstr, target.value + first_arg[1]).concat(first_arg.sexp_body(2))
246
+ exp = Sexp.new(:dstr, target.value + first_arg[1]).concat(first_arg.sexp_body(2)).line(exp.line)
240
247
  env[target_var] = exp
241
248
  elsif string? first_arg and string_interp? target
242
249
  if string? target.last
@@ -287,7 +294,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
287
294
 
288
295
  # Painful conversion of Array#join into string interpolation
289
296
  def process_array_join array, join_str
290
- result = s()
297
+ result = s().line(array.line)
291
298
 
292
299
  join_value = if string? join_str
293
300
  join_str.value
@@ -325,11 +332,11 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
325
332
  result.unshift combined_first
326
333
 
327
334
  # Have to fix up strings that follow interpolation
328
- result.reduce(s(:dstr)) do |memo, e|
335
+ result.reduce(s(:dstr).line(array.line)) do |memo, e|
329
336
  if string? e and node_type? memo.last, :evstr
330
337
  e.value = "#{join_value}#{e.value}"
331
338
  elsif join_value and node_type? memo.last, :evstr and node_type? e, :evstr
332
- memo << s(:str, join_value)
339
+ memo << s(:str, join_value).line(e.line)
333
340
  end
334
341
 
335
342
  memo << e
@@ -340,12 +347,29 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
340
347
  if item.is_a? String
341
348
  "#{item}#{join_value}"
342
349
  elsif string? item or symbol? item or number? item
343
- s(:str, "#{item.value}#{join_value}")
350
+ s(:str, "#{item.value}#{join_value}").line(item.line)
344
351
  else
345
- s(:evstr, item)
352
+ s(:evstr, item).line(item.line)
346
353
  end
347
354
  end
348
355
 
356
+ TEMP_FILE_CLASS = s(:const, :Tempfile)
357
+
358
+ def temp_file_open? exp
359
+ call? exp and
360
+ exp.target == TEMP_FILE_CLASS and
361
+ exp.method == :open
362
+ end
363
+
364
+ def temp_file_new line
365
+ s(:call, TEMP_FILE_CLASS, :new).line(line)
366
+ end
367
+
368
+ def splat_array? exp
369
+ node_type? exp, :splat and
370
+ node_type? exp[1], :array
371
+ end
372
+
349
373
  def process_iter exp
350
374
  @exp_context.push exp
351
375
  exp[1] = process exp.block_call
@@ -363,6 +387,9 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
363
387
  # Iterating over an array of all literal values
364
388
  local = Sexp.new(:lvar, block_args.last)
365
389
  env.current[local] = safe_literal(exp.line)
390
+ elsif temp_file_open? call
391
+ local = Sexp.new(:lvar, block_args.last)
392
+ env.current[local] = temp_file_new(exp.line)
366
393
  else
367
394
  block_args.each do |e|
368
395
  #Force block arg(s) to be local
@@ -663,7 +690,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
663
690
  end
664
691
  end
665
692
  else
666
- new_value = process s(:call, s(:call, target_var, :[], index), exp[3], value)
693
+ new_value = process s(:call, s(:call, target_var, :[], index), exp[3], value).line(exp.line)
667
694
 
668
695
  env[match] = new_value
669
696
  end
@@ -8,7 +8,7 @@ class Brakeman::BaseProcessor < Brakeman::SexpProcessor
8
8
  include Brakeman::SafeCallHelper
9
9
  include Brakeman::Util
10
10
 
11
- IGNORE = Sexp.new :ignore
11
+ IGNORE = Sexp.new(:ignore).line(0)
12
12
 
13
13
  #Return a new Processor.
14
14
  def initialize tracker
@@ -216,7 +216,7 @@ class Brakeman::BaseProcessor < Brakeman::SexpProcessor
216
216
  #
217
217
  #And also :layout for inside templates
218
218
  def find_render_type call, in_view = false
219
- rest = Sexp.new(:hash)
219
+ rest = Sexp.new(:hash).line(call.line)
220
220
  type = nil
221
221
  value = nil
222
222
  first_arg = call.first_arg
@@ -236,7 +236,7 @@ class Brakeman::BaseProcessor < Brakeman::SexpProcessor
236
236
  end
237
237
  elsif first_arg.is_a? Symbol or first_arg.is_a? String
238
238
  type = :action
239
- value = Sexp.new(:lit, first_arg.to_sym)
239
+ value = Sexp.new(:lit, first_arg.to_sym).line(call.line)
240
240
  elsif first_arg.nil?
241
241
  type = :default
242
242
  elsif not hash? first_arg
@@ -293,6 +293,6 @@ class Brakeman::BaseProcessor < Brakeman::SexpProcessor
293
293
  @tracker.processor.process_template(template_name, ast, type, nil, @current_file)
294
294
  @tracker.processor.process_template_alias(@tracker.templates[template_name])
295
295
 
296
- return s(:lit, template_name), options
296
+ return s(:lit, template_name).line(value.line), options
297
297
  end
298
298
  end
@@ -0,0 +1,64 @@
1
+ module Brakeman
2
+ class FileTypeDetector < BaseProcessor
3
+ def initialize
4
+ super(nil)
5
+ reset
6
+ end
7
+
8
+ def detect_type(file)
9
+ reset
10
+ process(file.ast)
11
+
12
+ if @file_type.nil?
13
+ @file_type = guess_from_path(file.path.relative)
14
+ end
15
+
16
+ @file_type || :libs
17
+ end
18
+
19
+ MODEL_CLASSES = [
20
+ :'ActiveRecord::Base',
21
+ :ApplicationRecord
22
+ ]
23
+
24
+ def process_class exp
25
+ name = class_name(exp.class_name)
26
+ parent = class_name(exp.parent_name)
27
+
28
+ if name.match(/Controller$/)
29
+ @file_type = :controllers
30
+ return exp
31
+ elsif MODEL_CLASSES.include? parent
32
+ @file_type = :models
33
+ return exp
34
+ end
35
+
36
+ super
37
+ end
38
+
39
+ def guess_from_path path
40
+ case
41
+ when path.include?('app/models')
42
+ :models
43
+ when path.include?('app/controllers')
44
+ :controllers
45
+ when path.include?('config/initializers')
46
+ :initializers
47
+ when path.include?('lib/')
48
+ :libs
49
+ when path.match?(%r{config/environments/(?!production\.rb)$})
50
+ :skip
51
+ when path.match?(%r{environments/production\.rb$})
52
+ :skip
53
+ when path.match?(%r{application\.rb$})
54
+ :skip
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def reset
61
+ @file_type = nil
62
+ end
63
+ end
64
+ end
@@ -57,6 +57,20 @@ class Brakeman::Rails3ConfigProcessor < Brakeman::BasicProcessor
57
57
  exp
58
58
  end
59
59
 
60
+ #Look for configuration settings that
61
+ #are just a call like
62
+ #
63
+ # config.load_defaults 5.2
64
+ def process_call exp
65
+ return exp unless @inside_config
66
+
67
+ if exp.target == RAILS_CONFIG and exp.first_arg
68
+ @tracker.config.rails[exp.method] = exp.first_arg
69
+ end
70
+
71
+ exp
72
+ end
73
+
60
74
  #Look for configuration settings
61
75
  def process_attrasgn exp
62
76
  return exp unless @inside_config
@@ -71,22 +85,8 @@ class Brakeman::Rails3ConfigProcessor < Brakeman::BasicProcessor
71
85
  @tracker.config.rails[attribute] = exp.first_arg
72
86
  end
73
87
  elsif include_rails_config? exp
74
- options = get_rails_config exp
75
- level = @tracker.config.rails
76
- options[0..-2].each do |o|
77
- level[o] ||= {}
78
-
79
- option = level[o]
80
-
81
- if not option.is_a? Hash
82
- Brakeman.debug "[Notice] Skipping config setting: #{options.map(&:to_s).join(".")}"
83
- return exp
84
- end
85
-
86
- level = level[o]
87
- end
88
-
89
- level[options.last] = exp.first_arg
88
+ options_path = get_rails_config exp
89
+ @tracker.config.set_rails_config(exp.first_arg, *options_path)
90
90
  end
91
91
 
92
92
  exp
@@ -2,10 +2,11 @@ require 'brakeman/processors/lib/rails3_config_processor'
2
2
 
3
3
  class Brakeman::Rails4ConfigProcessor < Brakeman::Rails3ConfigProcessor
4
4
  APPLICATION_CONFIG = s(:call, s(:call, s(:const, :Rails), :application), :configure)
5
+ ALT_APPLICATION_CONFIG = s(:call, s(:call, s(:colon3, :Rails), :application), :configure)
5
6
 
6
7
  # Look for Rails.application.configure do ... end
7
8
  def process_iter exp
8
- if exp.block_call == APPLICATION_CONFIG
9
+ if exp.block_call == APPLICATION_CONFIG or exp.block_call == ALT_APPLICATION_CONFIG
9
10
  @inside_config = true
10
11
  process exp.block if sexp? exp.block
11
12
  @inside_config = false
@@ -45,6 +45,9 @@ class Brakeman::Report
45
45
  Brakeman::Report::JUnit
46
46
  when :to_sarif
47
47
  return self.to_sarif
48
+ when :to_sonar
49
+ require_report 'sonar'
50
+ Brakeman::Report::Sonar
48
51
  else
49
52
  raise "Invalid format: #{format}. Should be one of #{VALID_FORMATS.inspect}"
50
53
  end
@@ -69,6 +72,11 @@ class Brakeman::Report
69
72
  generate Brakeman::Report::JSON
70
73
  end
71
74
 
75
+ def to_sonar
76
+ require_report 'sonar'
77
+ generate Brakeman::Report::Sonar
78
+ end
79
+
72
80
  def to_table
73
81
  require_report 'table'
74
82
  generate Brakeman::Report::Table