brakeman-lib 4.9.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +44 -0
  3. data/README.md +1 -1
  4. data/lib/brakeman.rb +10 -0
  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 +2 -1
  8. data/lib/brakeman/checks/check_model_attributes.rb +1 -1
  9. data/lib/brakeman/checks/check_regex_dos.rb +1 -1
  10. data/lib/brakeman/checks/check_sql.rb +2 -2
  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 +24 -18
  14. data/lib/brakeman/options.rb +5 -1
  15. data/lib/brakeman/parsers/template_parser.rb +2 -3
  16. data/lib/brakeman/processors/alias_processor.rb +20 -4
  17. data/lib/brakeman/processors/controller_processor.rb +1 -1
  18. data/lib/brakeman/processors/haml_template_processor.rb +8 -1
  19. data/lib/brakeman/processors/lib/call_conversion_helper.rb +1 -1
  20. data/lib/brakeman/processors/lib/file_type_detector.rb +64 -0
  21. data/lib/brakeman/processors/lib/rails3_config_processor.rb +16 -16
  22. data/lib/brakeman/processors/output_processor.rb +1 -1
  23. data/lib/brakeman/processors/template_alias_processor.rb +5 -0
  24. data/lib/brakeman/report.rb +15 -0
  25. data/lib/brakeman/report/report_base.rb +0 -2
  26. data/lib/brakeman/report/report_csv.rb +37 -60
  27. data/lib/brakeman/report/report_junit.rb +2 -2
  28. data/lib/brakeman/report/report_sarif.rb +114 -0
  29. data/lib/brakeman/report/report_sonar.rb +38 -0
  30. data/lib/brakeman/report/report_tabs.rb +1 -1
  31. data/lib/brakeman/report/report_text.rb +1 -1
  32. data/lib/brakeman/rescanner.rb +7 -5
  33. data/lib/brakeman/scanner.rb +44 -18
  34. data/lib/brakeman/tracker.rb +6 -0
  35. data/lib/brakeman/tracker/config.rb +76 -1
  36. data/lib/brakeman/tracker/controller.rb +1 -1
  37. data/lib/brakeman/util.rb +9 -4
  38. data/lib/brakeman/version.rb +1 -1
  39. data/lib/brakeman/warning.rb +10 -2
  40. data/lib/brakeman/warning_codes.rb +2 -0
  41. data/lib/ruby_parser/bm_sexp.rb +9 -9
  42. metadata +22 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43b7d1a166362e6f078be06194adde0e9acc6f6ee5bbe6b54212a4dddb0335ad
4
- data.tar.gz: fd3fcf8965f5125991e51dce67d18415ca3f5db6f431a4076c16acbf1a3bd906
3
+ metadata.gz: 59808cb56d2701ba4806eaf3e8fdaa70e3b15d3578badb1a93177ae60a03fe16
4
+ data.tar.gz: d74f4ffa7620d6ae48cddc85ad661548c6c21fa807306a987e31343990e33a09
5
5
  SHA512:
6
- metadata.gz: a929f04cb48c9ccb434cfa3ee47791d263a1fc3d30acdea4459c25c8c7bcab7d72887369f893de1eed5418a059dd07e55a98157bd2729967f1b9e4c72a4b94f5
7
- data.tar.gz: ead62901264f2d1230512894820ad8160ce6115a19ac32dd2ea3474ebb9b9a723e2090077ea6780b38c0b9d3f3b59e4c93ad55d8dbf613f9426475796a167ab3
6
+ metadata.gz: 1319c8e5a981305d8ab91885a9edfe475240014e7f3e19d2f4e24da8eabc0c0cf355cd8ef387091291dd4a7d3a29fc35309531e6edce6606100b5a7c48f24b64
7
+ data.tar.gz: 02356cced5a4aaae709a3f237319af4bf4f511224762c41bb61d1130813385d27e922722771d037ef1773271db642431be689f249b72396f7800730606ad3ba3
data/CHANGES.md CHANGED
@@ -1,3 +1,47 @@
1
+ # 5.0.0 - 2021-01-26
2
+
3
+ * Ignore `uuid` as a safe attribute
4
+ * Collapse `__send__` calls
5
+ * Ignore `Tempfile#path` in shell commands
6
+ * Ignore development environment
7
+ * Revamp CSV report to a CSV list of warnings
8
+ * Set Rails configuration defaults based on `load_defaults` version
9
+ * Add check for (more) unsafe method reflection
10
+ * Suggest using `--force` if no Rails application is detected
11
+ * Add Sonarqube report format (Adam England)
12
+ * Add check for potential HTTP verb confusion
13
+ * Add `--[no-]skip-vendor` option
14
+ * Scan (almost) all Ruby files in project
15
+
16
+ # 4.10.1 - 2020-12-24
17
+
18
+ * Declare REXML as a dependency (Ruby 3.0 compatibility)
19
+ * Use `Sexp#sexp_body` instead of `Sexp#[..]` (Ruby 3.0 compatibility)
20
+ * Prevent render loops when template names are absolute paths
21
+ * Ensure RubyParser is passed file path as a String
22
+ * Support new Haml 5.2.0 escaping method
23
+
24
+ # 5.0.0.pre1 - 2020-11-17
25
+
26
+ * Add check for (more) unsafe method reflection
27
+ * Suggest using `--force` if no Rails application is detected
28
+ * Add Sonarqube report format (Adam England)
29
+ * Add check for potential HTTP verb confusion
30
+ * Add `--[no-]skip-vendor` option
31
+ * Scan (almost) all Ruby files in project
32
+ * Add support for Haml 5.2.0
33
+
34
+ # 4.10.0 - 2020-09-28
35
+
36
+ * Add SARIF report format (Steve Winton)
37
+
38
+ # 4.9.1 - 2020-09-04
39
+
40
+ * Check `chomp`ed strings for SQL injection
41
+ * Use version from `active_record` for non-Rails apps (Ulysse Buonomo)
42
+ * Always set line number for joined arrays
43
+ * Avoid warning about missing `attr_accessible` if `protected_attributes` gem is used
44
+
1
45
  # 4.9.0 - 2020-08-04
2
46
 
3
47
  * Add check for CVE-2020-8166 (Jamie Finnigan)
data/README.md CHANGED
@@ -76,7 +76,7 @@ To specify an output file for the results:
76
76
 
77
77
  brakeman -o output_file
78
78
 
79
- The output format is determined by the file extension or by using the `-f` option. Current options are: `text`, `html`, `tabs`, `json`, `junit`, `markdown`, `csv`, and `codeclimate`.
79
+ The output format is determined by the file extension or by using the `-f` option. Current options are: `text`, `html`, `tabs`, `json`, `junit`, `markdown`, `csv`, `codeclimate`, and `sonar`.
80
80
 
81
81
  Multiple output files can be specified:
82
82
 
@@ -66,6 +66,7 @@ module Brakeman
66
66
  # * :run_checks - array of checks to run (run all if not specified)
67
67
  # * :safe_methods - array of methods to consider safe
68
68
  # * :skip_libs - do not process lib/ directory (default: false)
69
+ # * :skip_vendor - do not process vendor/ directory (default: true)
69
70
  # * :skip_checks - checks not to run (run all if not specified)
70
71
  # * :absolute_paths - show absolute path of each file (default: false)
71
72
  # * :summary_only - only output summary section of report for plain/table (:summary_only, :no_summary, true)
@@ -191,6 +192,7 @@ module Brakeman
191
192
  :report_progress => true,
192
193
  :safe_methods => Set.new,
193
194
  :skip_checks => Set.new,
195
+ :skip_vendor => true,
194
196
  }
195
197
  end
196
198
 
@@ -237,6 +239,10 @@ module Brakeman
237
239
  [:to_table]
238
240
  when :junit, :to_junit
239
241
  [:to_junit]
242
+ when :sarif, :to_sarif
243
+ [:to_sarif]
244
+ when :sonar, :to_sonar
245
+ [:to_sonar]
240
246
  else
241
247
  [:to_text]
242
248
  end
@@ -266,6 +272,10 @@ module Brakeman
266
272
  :to_table
267
273
  when /\.junit$/i
268
274
  :to_junit
275
+ when /\.sarif$/i
276
+ :to_sarif
277
+ when /\.sonar$/i
278
+ :to_sonar
269
279
  else
270
280
  :to_text
271
281
  end
@@ -21,6 +21,7 @@ module Brakeman
21
21
  end
22
22
  init_options[:additional_libs_path] = options[:additional_libs_path]
23
23
  init_options[:engine_paths] = options[:engine_paths]
24
+ init_options[:skip_vendor] = options[:skip_vendor]
24
25
  new(root, init_options)
25
26
  end
26
27
 
@@ -62,6 +63,7 @@ module Brakeman
62
63
  @engine_paths = init_options[:engine_paths] || []
63
64
  @absolute_engine_paths = @engine_paths.select { |path| path.start_with?(File::SEPARATOR) }
64
65
  @relative_engine_paths = @engine_paths - @absolute_engine_paths
66
+ @skip_vendor = init_options[:skip_vendor]
65
67
  @gemspec = nil
66
68
  @root_search_pattern = nil
67
69
  end
@@ -96,6 +98,10 @@ module Brakeman
96
98
  end
97
99
  end
98
100
 
101
+ def ruby_file_paths
102
+ find_paths(".").uniq
103
+ end
104
+
99
105
  def initializer_paths
100
106
  @initializer_paths ||= prioritize_concerns(find_paths("config/initializers"))
101
107
  end
@@ -109,8 +115,8 @@ module Brakeman
109
115
  end
110
116
 
111
117
  def template_paths
112
- @template_paths ||= find_paths("app/**/views", "*.{#{VIEW_EXTENSIONS}}") +
113
- find_paths("app/**/views", "*.{erb,haml,slim}").reject { |path| File.basename(path).count(".") > 1 }
118
+ @template_paths ||= find_paths(".", "*.{#{VIEW_EXTENSIONS}}") +
119
+ find_paths("**", "*.{erb,haml,slim}").reject { |path| File.basename(path).count(".") > 1 }
114
120
  end
115
121
 
116
122
  def layout_exists?(name)
@@ -163,7 +169,8 @@ module Brakeman
163
169
  def select_files(paths)
164
170
  paths = select_only_files(paths)
165
171
  paths = reject_skipped_files(paths)
166
- convert_to_file_paths(paths)
172
+ paths = convert_to_file_paths(paths)
173
+ reject_global_excludes(paths)
167
174
  end
168
175
 
169
176
  def select_only_files(paths)
@@ -182,6 +189,32 @@ module Brakeman
182
189
  end
183
190
  end
184
191
 
192
+ EXCLUDED_PATHS = %w[
193
+ /generators/
194
+ lib/tasks/
195
+ lib/templates/
196
+ db/
197
+ spec/
198
+ test/
199
+ tmp/
200
+ public/
201
+ log/
202
+ ]
203
+
204
+ def reject_global_excludes(paths)
205
+ paths.reject do |path|
206
+ relative_path = path.relative
207
+
208
+ if @skip_vendor and relative_path.include? 'vendor/'
209
+ true
210
+ else
211
+ EXCLUDED_PATHS.any? do |excluded|
212
+ relative_path.include? excluded
213
+ end
214
+ end
215
+ end
216
+ end
217
+
185
218
  def match_path files, path
186
219
  absolute_path = Pathname.new(path)
187
220
  # relative root never has a leading separator. But, we use a leading
@@ -40,7 +40,7 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
40
40
  @mass_assign_disabled = nil
41
41
  @has_user_input = nil
42
42
  @in_array = false
43
- @safe_input_attributes = Set[:to_i, :to_f, :arel_table, :id]
43
+ @safe_input_attributes = Set[:to_i, :to_f, :arel_table, :id, :uuid]
44
44
  @comparison_ops = Set[:==, :!=, :>, :<, :>=, :<=]
45
45
  end
46
46
 
@@ -151,6 +151,12 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
151
151
  method[-1] == "?"
152
152
  end
153
153
 
154
+ TEMP_FILE_PATH = s(:call, s(:call, s(:const, :Tempfile), :new), :path).freeze
155
+
156
+ def temp_file_path? exp
157
+ exp == TEMP_FILE_PATH
158
+ end
159
+
154
160
  #Report a warning
155
161
  def warn options
156
162
  extra_opts = { :check => self.class.to_s }
@@ -204,11 +204,12 @@ class Brakeman::CheckExecute < Brakeman::BaseCheck
204
204
  next if node_type? e, :lit, :str
205
205
  next if SAFE_VALUES.include? e
206
206
  next if shell_escape? e
207
+ next if temp_file_path? e
207
208
 
208
209
  if node_type? e, :if
209
210
  # If we're in a conditional, evaluate the `then` and `else` clauses to
210
211
  # see if they're dangerous.
211
- if res = dangerous?(e.values[1..-1])
212
+ if res = dangerous?(e.sexp_body.sexp_body)
212
213
  return res
213
214
  end
214
215
  elsif node_type? e, :or, :evstr, :dstr
@@ -8,7 +8,7 @@ class Brakeman::CheckModelAttributes < Brakeman::BaseCheck
8
8
  @description = "Reports models which do not use attr_restricted and warns on models that use attr_protected"
9
9
 
10
10
  def run_check
11
- return if mass_assign_disabled?
11
+ return if mass_assign_disabled? or tracker.config.has_gem?(:protected_attributes)
12
12
 
13
13
  #Roll warnings into one warning for all models
14
14
  if tracker.options[:collapse_mass_assignment]
@@ -29,7 +29,7 @@ class Brakeman::CheckRegexDoS < Brakeman::BaseCheck
29
29
  return unless original? result
30
30
 
31
31
  call = result[:call]
32
- components = call[1..-1]
32
+ components = call.sexp_body
33
33
 
34
34
  components.any? do |component|
35
35
  next unless sexp? component
@@ -393,7 +393,7 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
393
393
  nil
394
394
  end
395
395
 
396
- TO_STRING_METHODS = [:to_s, :squish, :strip, :strip_heredoc]
396
+ TO_STRING_METHODS = [:chomp, :to_s, :squish, :strip, :strip_heredoc]
397
397
 
398
398
  #Returns value if interpolated value is not something safe
399
399
  def unsafe_string_interp? exp
@@ -576,7 +576,7 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
576
576
  :sanitize_sql_for_assignment, :sanitize_sql_for_conditions, :sanitize_sql_hash,
577
577
  :sanitize_sql_hash_for_assignment, :sanitize_sql_hash_for_conditions,
578
578
  :to_sql, :sanitize, :primary_key, :table_name_prefix, :table_name_suffix,
579
- :where_values_hash, :foreign_key
579
+ :where_values_hash, :foreign_key, :uuid
580
580
  ]
581
581
 
582
582
  def safe_value? exp
@@ -0,0 +1,68 @@
1
+ require 'brakeman/checks/base_check'
2
+
3
+ class Brakeman::CheckUnsafeReflectionMethods < Brakeman::BaseCheck
4
+ Brakeman::Checks.add self
5
+
6
+ @description = "Checks for unsafe reflection to access methods"
7
+
8
+ def run_check
9
+ check_method
10
+ check_tap
11
+ check_to_proc
12
+ end
13
+
14
+ def check_method
15
+ tracker.find_call(method: :method, nested: true).each do |result|
16
+ argument = result[:call].first_arg
17
+
18
+ if user_input = include_user_input?(argument)
19
+ warn_unsafe_reflection(result, user_input)
20
+ end
21
+ end
22
+ end
23
+
24
+ def check_tap
25
+ tracker.find_call(method: :tap, nested: true).each do |result|
26
+ argument = result[:call].first_arg
27
+
28
+ # Argument is passed like a.tap(&argument)
29
+ if node_type? argument, :block_pass
30
+ argument = argument.value
31
+ end
32
+
33
+ if user_input = include_user_input?(argument)
34
+ warn_unsafe_reflection(result, user_input)
35
+ end
36
+ end
37
+ end
38
+
39
+ def check_to_proc
40
+ tracker.find_call(method: :to_proc, nested: true).each do |result|
41
+ target = result[:call].target
42
+
43
+ if user_input = include_user_input?(target)
44
+ warn_unsafe_reflection(result, user_input)
45
+ end
46
+ end
47
+ end
48
+
49
+ def warn_unsafe_reflection result, input
50
+ return unless original? result
51
+ method = result[:call].method
52
+
53
+ confidence = if input.type == :params
54
+ :high
55
+ else
56
+ :medium
57
+ end
58
+
59
+ message = msg("Unsafe reflection method ", msg_code(method), " called with ", msg_input(input))
60
+
61
+ warn :result => result,
62
+ :warning_type => "Remote Code Execution",
63
+ :warning_code => :unsafe_method_reflection,
64
+ :message => message,
65
+ :user_input => input,
66
+ :confidence => confidence
67
+ end
68
+ end
@@ -0,0 +1,75 @@
1
+ require 'brakeman/checks/base_check'
2
+
3
+ class Brakeman::CheckVerbConfusion < Brakeman::BaseCheck
4
+ Brakeman::Checks.add self
5
+
6
+ @description = "Check for uses of `request.get?` that might have unintentional behavior"
7
+
8
+ #Process calls
9
+ def run_check
10
+ calls = tracker.find_call(target: :request, methods: [:get?])
11
+
12
+ calls.each do |call|
13
+ process_result call
14
+ end
15
+ end
16
+
17
+ def process_result result
18
+ @current_result = result
19
+ @matched_call = result[:call]
20
+ klass = tracker.find_class(result[:location][:class])
21
+
22
+ # TODO: abstract into tracker.find_location ?
23
+ if klass.nil?
24
+ Brakeman.debug "No class found: #{result[:location][:class]}"
25
+ return
26
+ end
27
+
28
+ method = klass.get_method(result[:location][:method])
29
+
30
+ if method.nil?
31
+ Brakeman.debug "No method found: #{result[:location][:method]}"
32
+ return
33
+ end
34
+
35
+ process method[:src]
36
+ end
37
+
38
+ def process_if exp
39
+ if exp.condition == @matched_call
40
+ # Found `if request.get?`
41
+
42
+ # Do not warn if there is an `elsif` clause
43
+ if node_type? exp.else_clause, :if
44
+ return exp
45
+ end
46
+
47
+ warn_about_result @current_result, exp
48
+ end
49
+
50
+ exp
51
+ end
52
+
53
+ def warn_about_result result, code
54
+ return unless original? result
55
+
56
+ confidence = :weak
57
+ message = msg('Potential HTTP verb confusion. ',
58
+ msg_code('HEAD'),
59
+ ' is routed like ',
60
+ msg_code('GET'),
61
+ ' but ',
62
+ msg_code('request.get?'),
63
+ ' will return ',
64
+ msg_code('false')
65
+ )
66
+
67
+ warn :result => result,
68
+ :warning_type => "HTTP Verb Confusion",
69
+ :warning_code => :http_verb_confusion,
70
+ :message => message,
71
+ :code => code,
72
+ :user_input => result[:call],
73
+ :confidence => confidence
74
+ end
75
+ end
@@ -3,50 +3,56 @@ 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
35
34
 
35
+ # _path_ can be a string or a Brakeman::FilePath
36
36
  def parse_ruby input, path
37
+ if path.is_a? Brakeman::FilePath
38
+ path = path.relative
39
+ end
40
+
37
41
  begin
38
42
  Brakeman.debug "Parsing #{path}"
39
43
  RubyParser.new.parse input, path, @timeout
40
44
  rescue Racc::ParseError => e
41
- @tracker.error e, "Could not parse #{path}"
42
- nil
45
+ error e.exception(e.message + "\nCould not parse #{path}")
43
46
  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
47
+ error Exception.new("Parsing #{path} took too long (> #{@timeout} seconds). Try increasing the limit with --parser-timeout")
46
48
  rescue => e
47
- @tracker.error e.exception(e.message + "\nWhile processing #{path}"), e.backtrace
48
- nil
49
+ error e.exception(e.message + "\nWhile processing #{path}")
49
50
  end
50
51
  end
52
+
53
+ def error exception
54
+ @errors << exception
55
+ nil
56
+ end
51
57
  end
52
58
  end