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
@@ -0,0 +1,31 @@
1
+ # Github Actions Formatter
2
+ # Formats warnings as workflow commands to create annotations in GitHub UI
3
+ class Brakeman::Report::Github < Brakeman::Report::Base
4
+ def generate_report
5
+ # @see https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
6
+ errors.concat(warnings).join("\n")
7
+ end
8
+
9
+ def warnings
10
+ all_warnings
11
+ .map { |warning| "::warning file=#{warning_file(warning)},line=#{warning.line}::#{warning.message}" }
12
+ end
13
+
14
+ def errors
15
+ tracker.errors.map do |error|
16
+ if error[:exception].is_a?(Racc::ParseError)
17
+ # app/services/balance.rb:4 :: parse error on value "..." (tDOT3)
18
+ file, line = error[:exception].message.split(':').map(&:strip)[0,2]
19
+ "::error file=#{file},line=#{line}::#{clean_message(error[:error])}"
20
+ else
21
+ "::error ::#{clean_message(error[:error])}"
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def clean_message(msg)
29
+ msg.gsub('::','').squeeze(' ')
30
+ end
31
+ end
@@ -47,7 +47,7 @@ class Brakeman::Report::JUnit < Brakeman::Report::Base
47
47
  warning.add_attribute 'brakeman:file', warning_file(w)
48
48
  warning.add_attribute 'brakeman:line', w.line
49
49
  warning.add_attribute 'brakeman:fingerprint', w.fingerprint
50
- warning.add_attribute 'brakeman:confidence', TEXT_CONFIDENCE[w.confidence]
50
+ warning.add_attribute 'brakeman:confidence', w.confidence_name
51
51
  warning.add_attribute 'brakeman:code', w.format_code
52
52
  warning.add_text w.to_s
53
53
  }
@@ -88,7 +88,7 @@ class Brakeman::Report::JUnit < Brakeman::Report::Base
88
88
  failure.add_attribute 'brakeman:fingerprint', warning.fingerprint
89
89
  failure.add_attribute 'brakeman:file', warning_file(warning)
90
90
  failure.add_attribute 'brakeman:line', warning.line
91
- failure.add_attribute 'brakeman:confidence', TEXT_CONFIDENCE[warning.confidence]
91
+ failure.add_attribute 'brakeman:confidence', warning.confidence_name
92
92
  failure.add_attribute 'brakeman:code', warning.format_code
93
93
  failure.add_text warning.to_s
94
94
  }
@@ -27,7 +27,7 @@ class Brakeman::Report::SARIF < Brakeman::Report::Base
27
27
  def rules
28
28
  @rules ||= unique_warnings_by_warning_code.map do |warning|
29
29
  rule_id = render_id warning
30
- check_name = warning.check.gsub(/^Brakeman::Check/, '')
30
+ check_name = warning.check_name
31
31
  check_description = render_message check_descriptions[check_name]
32
32
  {
33
33
  :id => rule_id,
@@ -0,0 +1,38 @@
1
+ class Brakeman::Report::Sonar < Brakeman::Report::Base
2
+ def generate_report
3
+ report_object = {
4
+ issues: all_warnings.map { |warning| issue_json(warning) }
5
+ }
6
+ return JSON.pretty_generate report_object
7
+ end
8
+
9
+ private
10
+
11
+ def issue_json(warning)
12
+ {
13
+ engineId: "Brakeman",
14
+ ruleId: warning.warning_code,
15
+ type: "VULNERABILITY",
16
+ severity: severity_level_for(warning.confidence),
17
+ primaryLocation: {
18
+ message: warning.message,
19
+ filePath: warning.file.relative,
20
+ textRange: {
21
+ "startLine": warning.line || 1,
22
+ "endLine": warning.line || 1,
23
+ }
24
+ },
25
+ effortMinutes: (4 - warning.confidence) * 15
26
+ }
27
+ end
28
+
29
+ def severity_level_for(confidence)
30
+ if confidence == 0
31
+ "CRITICAL"
32
+ elsif confidence == 1
33
+ "MAJOR"
34
+ else
35
+ "MINOR"
36
+ end
37
+ end
38
+ end
@@ -10,7 +10,7 @@ class Brakeman::Report::Tabs < Brakeman::Report::Table
10
10
  self.send(meth).map do |w|
11
11
  line = w.line || 0
12
12
  w.warning_type.gsub!(/[^\w\s]/, ' ')
13
- "#{(w.file.absolute)}\t#{line}\t#{w.warning_type}\t#{category}\t#{w.format_message}\t#{TEXT_CONFIDENCE[w.confidence]}"
13
+ "#{(w.file.absolute)}\t#{line}\t#{w.warning_type}\t#{category}\t#{w.format_message}\t#{w.confidence_name}"
14
14
  end.join "\n"
15
15
 
16
16
  end.join "\n"
@@ -160,7 +160,7 @@ class Brakeman::Report::Text < Brakeman::Report::Base
160
160
  when :category
161
161
  label('Category', w.warning_type.to_s)
162
162
  when :check
163
- label('Check', w.check.gsub(/^Brakeman::Check/, ''))
163
+ label('Check', w.check_name)
164
164
  when :message
165
165
  label('Message', w.message)
166
166
  when :code
@@ -132,10 +132,11 @@ class Brakeman::Rescanner < Brakeman::Scanner
132
132
  template_name = template_path_to_name(path)
133
133
 
134
134
  tracker.reset_template template_name
135
- fp = Brakeman::FileParser.new(tracker)
135
+ fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout])
136
136
  template_parser = Brakeman::TemplateParser.new(tracker, fp)
137
137
  template_parser.parse_template path, path.read
138
- process_template fp.file_list[:templates].first
138
+ tracker.add_errors(fp.errors)
139
+ process_template fp.file_list.first
139
140
 
140
141
  @processor.process_template_alias tracker.templates[template_name]
141
142
 
@@ -390,9 +391,10 @@ class Brakeman::Rescanner < Brakeman::Scanner
390
391
 
391
392
  def parse_ruby_files list
392
393
  paths = list.select(&:exists?)
393
- file_parser = Brakeman::FileParser.new(tracker)
394
- file_parser.parse_files paths, :rescan
395
- file_parser.file_list[:rescan]
394
+ file_parser = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout])
395
+ file_parser.parse_files paths
396
+ tracker.add_errors(file_parser.errors)
397
+ file_parser.file_list
396
398
  end
397
399
  end
398
400
 
@@ -7,6 +7,7 @@ begin
7
7
  require 'brakeman/app_tree'
8
8
  require 'brakeman/file_parser'
9
9
  require 'brakeman/parsers/template_parser'
10
+ require 'brakeman/processors/lib/file_type_detector'
10
11
  rescue LoadError => e
11
12
  $stderr.puts e.message
12
13
  $stderr.puts "Please install the appropriate dependency."
@@ -23,7 +24,10 @@ class Brakeman::Scanner
23
24
  @app_tree = Brakeman::AppTree.from_options(options)
24
25
 
25
26
  if (!@app_tree.root || !@app_tree.exists?("app")) && !options[:force_scan]
26
- raise Brakeman::NoApplication, "Please supply the path to a Rails application (looking in #{@app_tree.root})."
27
+ message = "Please supply the path to a Rails application (looking in #{@app_tree.root}).\n" <<
28
+ " Use `--force` to run a scan anyway."
29
+
30
+ raise Brakeman::NoApplication, message
27
31
  end
28
32
 
29
33
  @processor = processor || Brakeman::Processor.new(@app_tree, options)
@@ -43,6 +47,8 @@ class Brakeman::Scanner
43
47
  process_config
44
48
  Brakeman.notify "Parsing files..."
45
49
  parse_files
50
+ Brakeman.notify "Detecting file types..."
51
+ detect_file_types
46
52
  Brakeman.notify "Processing initializers..."
47
53
  process_initializers
48
54
  Brakeman.notify "Processing libs..."
@@ -65,29 +71,47 @@ class Brakeman::Scanner
65
71
  end
66
72
 
67
73
  def parse_files
68
- fp = Brakeman::FileParser.new tracker
69
-
70
- files = {
71
- :initializers => @app_tree.initializer_paths,
72
- :controllers => @app_tree.controller_paths,
73
- :models => @app_tree.model_paths
74
- }
75
-
76
- unless options[:skip_libs]
77
- files[:libs] = @app_tree.lib_paths
78
- end
74
+ fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout])
79
75
 
80
- files.each do |name, paths|
81
- fp.parse_files paths, name
82
- end
76
+ fp.parse_files tracker.app_tree.ruby_file_paths
83
77
 
84
78
  template_parser = Brakeman::TemplateParser.new(tracker, fp)
85
79
 
86
- fp.read_files(@app_tree.template_paths, :templates) do |path, contents|
80
+ fp.read_files(@app_tree.template_paths) do |path, contents|
87
81
  template_parser.parse_template path, contents
88
82
  end
89
83
 
90
- @file_list = fp.file_list
84
+ # Collect errors raised during parsing
85
+ tracker.add_errors(fp.errors)
86
+
87
+ @parsed_files = fp.file_list
88
+ end
89
+
90
+ def detect_file_types
91
+ @file_list = {
92
+ controllers: [],
93
+ initializers: [],
94
+ libs: [],
95
+ models: [],
96
+ templates: [],
97
+ }
98
+
99
+ detector = Brakeman::FileTypeDetector.new
100
+
101
+ @parsed_files.each do |file|
102
+ if file.is_a? Brakeman::TemplateParser::TemplateFile
103
+ @file_list[:templates] << file
104
+ else
105
+ type = detector.detect_type(file)
106
+ unless type == :skip
107
+ if @file_list[type].nil?
108
+ raise type.to_s
109
+ else
110
+ @file_list[type] << file
111
+ end
112
+ end
113
+ end
114
+ end
91
115
  end
92
116
 
93
117
  #Process config/environment.rb and config/gems.rb
@@ -115,6 +139,8 @@ class Brakeman::Scanner
115
139
  if @app_tree.exists? ".ruby-version"
116
140
  tracker.config.set_ruby_version @app_tree.file_path(".ruby-version").read
117
141
  end
142
+
143
+ tracker.config.load_rails_defaults
118
144
  end
119
145
 
120
146
  def process_config_file file
@@ -325,8 +351,11 @@ class Brakeman::Scanner
325
351
  end
326
352
 
327
353
  def parse_ruby_file file
328
- fp = Brakeman::FileParser.new(self.tracker)
354
+ fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout])
329
355
  fp.parse_ruby(file.read, file)
356
+ rescue Exception => e
357
+ tracker.error(e)
358
+ nil
330
359
  end
331
360
  end
332
361
 
@@ -35,6 +35,7 @@ class Brakeman::Tracker
35
35
  #class they are.
36
36
  @models = {}
37
37
  @models[UNKNOWN_MODEL] = Brakeman::Model.new(UNKNOWN_MODEL, nil, @app_tree.file_path("NOT_REAL.rb"), nil, self)
38
+ @method_cache = {}
38
39
  @routes = {}
39
40
  @initializers = {}
40
41
  @errors = []
@@ -68,6 +69,12 @@ class Brakeman::Tracker
68
69
  }
69
70
  end
70
71
 
72
+ def add_errors exceptions
73
+ exceptions.each do |e|
74
+ error(e)
75
+ end
76
+ end
77
+
71
78
  #Run a set of checks on the current information. Results will be stored
72
79
  #in Tracker#checks.
73
80
  def run_checks
@@ -93,8 +100,8 @@ class Brakeman::Tracker
93
100
  classes.each do |set|
94
101
  set.each do |set_name, collection|
95
102
  collection.each_method do |method_name, definition|
96
- src = definition[:src]
97
- yield src, set_name, method_name, definition[:file]
103
+ src = definition.src
104
+ yield src, set_name, method_name, definition.file
98
105
  end
99
106
  end
100
107
  end
@@ -214,6 +221,34 @@ class Brakeman::Tracker
214
221
  nil
215
222
  end
216
223
 
224
+ def find_method method_name, class_name, method_type = :instance
225
+ return nil unless method_name.is_a? Symbol
226
+
227
+ klass = find_class(class_name)
228
+ return nil unless klass
229
+
230
+ cache_key = [klass, method_name, method_type]
231
+
232
+ if method = @method_cache[cache_key]
233
+ return method
234
+ end
235
+
236
+ if method = klass.get_method(method_name, method_type)
237
+ return method
238
+ else
239
+ # Check modules included for method definition
240
+ # TODO: only for instance methods, otherwise check extends!
241
+ klass.includes.each do |included_name|
242
+ if method = find_method(method_name, included_name, method_type)
243
+ return (@method_cache[cache_key] = method)
244
+ end
245
+ end
246
+
247
+ # Not in any included modules, check the parent
248
+ @method_cache[cache_key] = find_method(method_name, klass.parent)
249
+ end
250
+ end
251
+
217
252
  def index_call_sites
218
253
  finder = Brakeman::FindAllCalls.new self
219
254
 
@@ -279,8 +314,8 @@ class Brakeman::Tracker
279
314
  method_sets.each do |set|
280
315
  set.each do |set_name, info|
281
316
  info.each_method do |method_name, definition|
282
- src = definition[:src]
283
- finder.process_source src, :class => set_name, :method => method_name, :file => definition[:file]
317
+ src = definition.src
318
+ finder.process_source src, :class => set_name, :method => method_name, :file => definition.file
284
319
  end
285
320
  end
286
321
  end
@@ -1,4 +1,5 @@
1
1
  require 'brakeman/util'
2
+ require 'brakeman/tracker/method_info'
2
3
 
3
4
  module Brakeman
4
5
  class Collection
@@ -13,6 +14,7 @@ module Brakeman
13
14
  @src = {}
14
15
  @includes = []
15
16
  @methods = { :public => {}, :private => {}, :protected => {} }
17
+ @class_methods = {}
16
18
  @options = {}
17
19
  @tracker = tracker
18
20
 
@@ -46,11 +48,16 @@ module Brakeman
46
48
  end
47
49
 
48
50
  def add_method visibility, name, src, file_name
51
+ meth_info = Brakeman::MethodInfo.new(name, src, self, file_name)
52
+
49
53
  if src.node_type == :defs
54
+ @class_methods[name] = meth_info
55
+
56
+ # TODO fix this weirdness
50
57
  name = :"#{src[1]}.#{name}"
51
58
  end
52
59
 
53
- @methods[visibility][name] = { :src => src, :file => file_name }
60
+ @methods[visibility][name] = meth_info
54
61
  end
55
62
 
56
63
  def each_method
@@ -61,16 +68,31 @@ module Brakeman
61
68
  end
62
69
  end
63
70
 
64
- def get_method name
65
- each_method do |n, info|
66
- if n == name
67
- return info
71
+ def get_method name, type = :instance
72
+ case type
73
+ when :class
74
+ get_class_method name
75
+ when :instance
76
+ get_instance_method name
77
+ else
78
+ raise "Unexpected method type: #{type.inspect}"
79
+ end
80
+ end
81
+
82
+ def get_instance_method name
83
+ @methods.each do |_vis, meths|
84
+ if meths[name]
85
+ return meths[name]
68
86
  end
69
87
  end
70
88
 
71
89
  nil
72
90
  end
73
91
 
92
+ def get_class_method name
93
+ @class_methods[name]
94
+ end
95
+
74
96
  def file
75
97
  @files.first
76
98
  end
@@ -149,5 +149,78 @@ module Brakeman
149
149
  def session_settings
150
150
  @rails.dig(:action_controller, :session)
151
151
  end
152
+
153
+
154
+ # Set Rails config option value
155
+ # where path is an array of attributes, e.g.
156
+ #
157
+ # :action_controller, :perform_caching
158
+ #
159
+ # then this will set
160
+ #
161
+ # rails[:action_controller][:perform_caching] = value
162
+ def set_rails_config value, *path
163
+ config = self.rails
164
+
165
+ path[0..-2].each do |o|
166
+ config[o] ||= {}
167
+
168
+ option = config[o]
169
+
170
+ if not option.is_a? Hash
171
+ Brakeman.debug "[Notice] Skipping config setting: #{path.map(&:to_s).join(".")}"
172
+ return
173
+ end
174
+
175
+ config = option
176
+ end
177
+
178
+ config[path.last] = value
179
+ end
180
+
181
+ # Load defaults based on config.load_defaults value
182
+ # as documented here: https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults
183
+ def load_rails_defaults
184
+ return unless number? tracker.config.rails[:load_defaults]
185
+
186
+ version = tracker.config.rails[:load_defaults].value
187
+ true_value = Sexp.new(:true)
188
+ false_value = Sexp.new(:false)
189
+
190
+ if version >= 5.0
191
+ set_rails_config(true_value, :action_controller, :per_form_csrf_tokens)
192
+ set_rails_config(true_value, :action_controller, :forgery_protection_origin_check)
193
+ set_rails_config(true_value, :active_record, :belongs_to_required_by_default)
194
+ # Note: this may need to be changed, because ssl_options is a Hash
195
+ set_rails_config(true_value, :ssl_options, :hsts, :subdomains)
196
+ end
197
+
198
+ if version >= 5.1
199
+ set_rails_config(false_value, :assets, :unknown_asset_fallback)
200
+ set_rails_config(true_value, :action_view, :form_with_generates_remote_forms)
201
+ end
202
+
203
+ if version >= 5.2
204
+ set_rails_config(true_value, :active_record, :cache_versioning)
205
+ set_rails_config(true_value, :action_dispatch, :use_authenticated_cookie_encryption)
206
+ set_rails_config(true_value, :active_support, :use_authenticated_message_encryption)
207
+ set_rails_config(true_value, :active_support, :use_sha1_digests)
208
+ set_rails_config(true_value, :action_controller, :default_protect_from_forgery)
209
+ set_rails_config(true_value, :action_view, :form_with_generates_ids)
210
+ end
211
+
212
+ if version >= 6.0
213
+ set_rails_config(Sexp.new(:lit, :zeitwerk), :autoloader)
214
+ set_rails_config(false_value, :action_view, :default_enforce_utf8)
215
+ set_rails_config(true_value, :action_dispatch, :use_cookies_with_metadata)
216
+ set_rails_config(false_value, :action_dispatch, :return_only_media_type_on_content_type)
217
+ set_rails_config(Sexp.new(:str, 'ActionMailer::MailDeliveryJob'), :action_mailer, :delivery_job)
218
+ set_rails_config(true_value, :active_job, :return_false_on_aborted_enqueue)
219
+ set_rails_config(Sexp.new(:lit, :active_storage_analysis), :active_storage, :queues, :analysis)
220
+ set_rails_config(Sexp.new(:lit, :active_storage_purge), :active_storage, :queues, :purge)
221
+ set_rails_config(true_value, :active_storage, :replace_on_assign_to_many)
222
+ set_rails_config(true_value, :active_record, :collection_cache_versioning)
223
+ end
224
+ end
152
225
  end
153
226
  end