brakeman-lib 4.10.0 → 5.0.2

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +46 -0
  3. data/README.md +11 -2
  4. data/lib/brakeman.rb +21 -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_detailed_exceptions.rb +1 -1
  8. data/lib/brakeman/checks/check_evaluation.rb +1 -1
  9. data/lib/brakeman/checks/check_execute.rb +2 -1
  10. data/lib/brakeman/checks/check_mass_assignment.rb +4 -6
  11. data/lib/brakeman/checks/check_regex_dos.rb +1 -1
  12. data/lib/brakeman/checks/check_sanitize_methods.rb +2 -1
  13. data/lib/brakeman/checks/check_sql.rb +16 -3
  14. data/lib/brakeman/checks/check_unsafe_reflection_methods.rb +68 -0
  15. data/lib/brakeman/checks/check_verb_confusion.rb +75 -0
  16. data/lib/brakeman/file_parser.rb +50 -22
  17. data/lib/brakeman/options.rb +5 -1
  18. data/lib/brakeman/parsers/template_parser.rb +26 -3
  19. data/lib/brakeman/processors/alias_processor.rb +91 -19
  20. data/lib/brakeman/processors/base_processor.rb +4 -4
  21. data/lib/brakeman/processors/controller_alias_processor.rb +6 -43
  22. data/lib/brakeman/processors/controller_processor.rb +1 -1
  23. data/lib/brakeman/processors/haml_template_processor.rb +8 -1
  24. data/lib/brakeman/processors/lib/call_conversion_helper.rb +10 -0
  25. data/lib/brakeman/processors/lib/file_type_detector.rb +64 -0
  26. data/lib/brakeman/processors/lib/rails3_config_processor.rb +16 -16
  27. data/lib/brakeman/processors/lib/rails4_config_processor.rb +2 -1
  28. data/lib/brakeman/processors/library_processor.rb +9 -0
  29. data/lib/brakeman/processors/output_processor.rb +1 -1
  30. data/lib/brakeman/processors/template_alias_processor.rb +5 -0
  31. data/lib/brakeman/report.rb +12 -1
  32. data/lib/brakeman/report/ignore/interactive.rb +1 -1
  33. data/lib/brakeman/report/report_base.rb +0 -2
  34. data/lib/brakeman/report/report_csv.rb +37 -60
  35. data/lib/brakeman/report/report_github.rb +31 -0
  36. data/lib/brakeman/report/report_junit.rb +2 -2
  37. data/lib/brakeman/report/report_sarif.rb +1 -1
  38. data/lib/brakeman/report/report_sonar.rb +38 -0
  39. data/lib/brakeman/report/report_tabs.rb +1 -1
  40. data/lib/brakeman/report/report_text.rb +1 -1
  41. data/lib/brakeman/rescanner.rb +7 -5
  42. data/lib/brakeman/scanner.rb +47 -18
  43. data/lib/brakeman/tracker.rb +39 -4
  44. data/lib/brakeman/tracker/collection.rb +27 -5
  45. data/lib/brakeman/tracker/config.rb +73 -0
  46. data/lib/brakeman/tracker/controller.rb +1 -1
  47. data/lib/brakeman/tracker/method_info.rb +29 -0
  48. data/lib/brakeman/util.rb +17 -4
  49. data/lib/brakeman/version.rb +1 -1
  50. data/lib/brakeman/warning.rb +10 -2
  51. data/lib/brakeman/warning_codes.rb +2 -0
  52. data/lib/ruby_parser/bm_sexp.rb +9 -9
  53. metadata +39 -5
@@ -1,51 +1,79 @@
1
+ require 'parallel'
2
+
1
3
  module Brakeman
2
4
  ASTFile = Struct.new(:path, :ast)
3
5
 
4
6
  # This class handles reading and parsing files.
5
7
  class FileParser
6
- attr_reader :file_list
8
+ attr_reader :file_list, :errors
7
9
 
8
- def initialize tracker
9
- @tracker = tracker
10
- @timeout = @tracker.options[:parser_timeout]
11
- @app_tree = @tracker.app_tree
12
- @file_list = {}
10
+ def initialize app_tree, timeout
11
+ @app_tree = app_tree
12
+ @timeout = timeout
13
+ @file_list = []
14
+ @errors = []
13
15
  end
14
16
 
15
- def parse_files list, type
16
- read_files list, type do |path, contents|
17
- if ast = parse_ruby(contents, path.relative)
18
- ASTFile.new(path, ast)
17
+ def parse_files list
18
+ # Parse the files in parallel.
19
+ # By default, the parsing will be in separate processes.
20
+ # So we map the result to ASTFiles and/or Exceptions
21
+ # then partition them into ASTFiles and Exceptions
22
+ # and add the Exceptions to @errors
23
+ #
24
+ # Basically just a funky way to deal with two possible
25
+ # return types that are returned from isolated processes.
26
+ #
27
+ # Note this method no longer uses read_files
28
+ @file_list, new_errors = Parallel.map(list) do |file_name|
29
+ file_path = @app_tree.file_path(file_name)
30
+ contents = file_path.read
31
+
32
+ begin
33
+ if ast = parse_ruby(contents, file_path.relative)
34
+ ASTFile.new(file_name, ast)
35
+ end
36
+ rescue Exception => e
37
+ e
19
38
  end
39
+ end.compact.partition do |result|
40
+ result.is_a? ASTFile
20
41
  end
21
- end
22
42
 
23
- def read_files list, type
24
- @file_list[type] ||= []
43
+ errors.concat new_errors
44
+ end
25
45
 
46
+ def read_files list
26
47
  list.each do |path|
27
48
  file = @app_tree.file_path(path)
28
49
 
29
- result = yield file, file.read
30
- if result
31
- @file_list[type] << result
50
+ begin
51
+ result = yield file, file.read
52
+
53
+ if result
54
+ @file_list << result
55
+ end
56
+ rescue Exception => e
57
+ @errors << e
32
58
  end
33
59
  end
34
60
  end
35
61
 
62
+ # _path_ can be a string or a Brakeman::FilePath
36
63
  def parse_ruby input, path
64
+ if path.is_a? Brakeman::FilePath
65
+ path = path.relative
66
+ end
67
+
37
68
  begin
38
69
  Brakeman.debug "Parsing #{path}"
39
70
  RubyParser.new.parse input, path, @timeout
40
71
  rescue Racc::ParseError => e
41
- @tracker.error e, "Could not parse #{path}"
42
- nil
72
+ raise e.exception(e.message + "\nCould not parse #{path}")
43
73
  rescue Timeout::Error => e
44
- @tracker.error Exception.new("Parsing #{path} took too long (> #{@timeout} seconds). Try increasing the limit with --parser-timeout"), caller
45
- nil
74
+ raise Exception.new("Parsing #{path} took too long (> #{@timeout} seconds). Try increasing the limit with --parser-timeout")
46
75
  rescue => e
47
- @tracker.error e.exception(e.message + "\nWhile processing #{path}"), e.backtrace
48
- nil
76
+ raise e.exception(e.message + "\nWhile processing #{path}")
49
77
  end
50
78
  end
51
79
  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, :github],
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
@@ -213,13 +220,28 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
213
220
  exp = math_op(:+, target, first_arg, exp)
214
221
  end
215
222
  when :-, :*, :/
216
- exp = math_op(method, target, first_arg, exp)
223
+ if method == :* and array? target
224
+ if string? first_arg
225
+ exp = process_array_join(target, first_arg)
226
+ end
227
+ else
228
+ exp = math_op(method, target, first_arg, exp)
229
+ end
217
230
  when :[]
218
231
  if array? target
219
232
  exp = process_array_access(target, exp.args, exp)
220
233
  elsif hash? target
221
234
  exp = process_hash_access(target, first_arg, exp)
222
235
  end
236
+ when :fetch
237
+ if array? target
238
+ # Not dealing with default value
239
+ # so just pass in first argument, but process_array_access expects
240
+ # an array of arguments.
241
+ exp = process_array_access(target, [first_arg], exp)
242
+ elsif hash? target
243
+ exp = process_hash_access(target, first_arg, exp)
244
+ end
223
245
  when :merge!, :update
224
246
  if hash? target and hash? first_arg
225
247
  target = process_hash_merge! target, first_arg
@@ -236,7 +258,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
236
258
  env[target_var] = target
237
259
  return target
238
260
  elsif string? target and string_interp? first_arg
239
- exp = Sexp.new(:dstr, target.value + first_arg[1]).concat(first_arg[2..-1])
261
+ exp = Sexp.new(:dstr, target.value + first_arg[1]).concat(first_arg.sexp_body(2)).line(exp.line)
240
262
  env[target_var] = exp
241
263
  elsif string? first_arg and string_interp? target
242
264
  if string? target.last
@@ -259,6 +281,12 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
259
281
  target = find_push_target(target_var)
260
282
  env[target] = exp unless target.nil? # Happens in TemplateAliasProcessor
261
283
  end
284
+ when :push
285
+ if array? target
286
+ target << first_arg
287
+ env[target_var] = target
288
+ return target
289
+ end
262
290
  when :first
263
291
  if array? target and first_arg.nil? and sexp? target[1]
264
292
  exp = target[1]
@@ -272,7 +300,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
272
300
  exp = target
273
301
  end
274
302
  when :join
275
- if array? target and target.length > 2 and (string? first_arg or first_arg.nil?)
303
+ if array? target and (string? first_arg or first_arg.nil?)
276
304
  exp = process_array_join(target, first_arg)
277
305
  end
278
306
  when :!
@@ -280,6 +308,15 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
280
308
  if call? target and target.method == :!
281
309
  exp = s(:or, s(:true).line(exp.line), s(:false).line(exp.line)).line(exp.line)
282
310
  end
311
+ when :values
312
+ # Hash literal
313
+ if node_type? target, :hash
314
+ exp = hash_values(target)
315
+ end
316
+ when :values_at
317
+ if hash? target
318
+ exp = hash_values_at target, exp.args
319
+ end
283
320
  end
284
321
 
285
322
  exp
@@ -287,7 +324,12 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
287
324
 
288
325
  # Painful conversion of Array#join into string interpolation
289
326
  def process_array_join array, join_str
290
- result = s()
327
+ # Empty array
328
+ if array.length == 1
329
+ return s(:str, '').line(array.line)
330
+ end
331
+
332
+ result = s().line(array.line)
291
333
 
292
334
  join_value = if string? join_str
293
335
  join_str.value
@@ -295,8 +337,10 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
295
337
  nil
296
338
  end
297
339
 
298
- array[1..-2].each do |e|
299
- result << join_item(e, join_value)
340
+ if array.length > 2
341
+ array[1..-2].each do |e|
342
+ result << join_item(e, join_value)
343
+ end
300
344
  end
301
345
 
302
346
  result << join_item(array.last, nil)
@@ -325,27 +369,52 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
325
369
  result.unshift combined_first
326
370
 
327
371
  # Have to fix up strings that follow interpolation
328
- result.reduce(s(:dstr)) do |memo, e|
372
+ string = result.reduce(s(:dstr).line(array.line)) do |memo, e|
329
373
  if string? e and node_type? memo.last, :evstr
330
374
  e.value = "#{join_value}#{e.value}"
331
375
  elsif join_value and node_type? memo.last, :evstr and node_type? e, :evstr
332
- memo << s(:str, join_value)
376
+ memo << s(:str, join_value).line(e.line)
333
377
  end
334
378
 
335
379
  memo << e
336
380
  end
381
+
382
+ # Convert (:dstr, "hello world")
383
+ # to (:str, "hello world")
384
+ if string.length == 2 and string.last.is_a? String
385
+ string[0] = :str
386
+ end
387
+
388
+ string
337
389
  end
338
390
 
339
391
  def join_item item, join_value
340
392
  if item.is_a? String
341
393
  "#{item}#{join_value}"
342
394
  elsif string? item or symbol? item or number? item
343
- s(:str, "#{item.value}#{join_value}")
395
+ s(:str, "#{item.value}#{join_value}").line(item.line)
344
396
  else
345
- s(:evstr, item)
397
+ s(:evstr, item).line(item.line)
346
398
  end
347
399
  end
348
400
 
401
+ TEMP_FILE_CLASS = s(:const, :Tempfile)
402
+
403
+ def temp_file_open? exp
404
+ call? exp and
405
+ exp.target == TEMP_FILE_CLASS and
406
+ exp.method == :open
407
+ end
408
+
409
+ def temp_file_new line
410
+ s(:call, TEMP_FILE_CLASS, :new).line(line)
411
+ end
412
+
413
+ def splat_array? exp
414
+ node_type? exp, :splat and
415
+ node_type? exp[1], :array
416
+ end
417
+
349
418
  def process_iter exp
350
419
  @exp_context.push exp
351
420
  exp[1] = process exp.block_call
@@ -363,6 +432,9 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
363
432
  # Iterating over an array of all literal values
364
433
  local = Sexp.new(:lvar, block_args.last)
365
434
  env.current[local] = safe_literal(exp.line)
435
+ elsif temp_file_open? call
436
+ local = Sexp.new(:lvar, block_args.last)
437
+ env.current[local] = temp_file_new(exp.line)
366
438
  else
367
439
  block_args.each do |e|
368
440
  #Force block arg(s) to be local
@@ -663,7 +735,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
663
735
  end
664
736
  end
665
737
  else
666
- new_value = process s(:call, s(:call, target_var, :[], index), exp[3], value)
738
+ new_value = process s(:call, s(:call, target_var, :[], index), exp[3], value).line(exp.line)
667
739
 
668
740
  env[match] = new_value
669
741
  end
@@ -941,7 +1013,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
941
1013
  args = exp.args
942
1014
  exp.pop # remove last arg
943
1015
  if args.length > 1
944
- exp.arglist = args[1..-1]
1016
+ exp.arglist = args.sexp_body
945
1017
  end
946
1018
  end
947
1019
 
@@ -986,8 +1058,8 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
986
1058
  method_name = call.method
987
1059
 
988
1060
  #Look for helper methods and see if we can get a return value
989
- if found_method = find_method(method_name, @current_class)
990
- helper = found_method[:method]
1061
+ if found_method = tracker.find_method(method_name, @current_class)
1062
+ helper = found_method.src
991
1063
 
992
1064
  if sexp? helper
993
1065
  value = process_helper_method helper, call.args
@@ -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