brakeman-lib 4.7.1 → 4.9.0

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +47 -0
  3. data/README.md +13 -5
  4. data/lib/brakeman.rb +20 -0
  5. data/lib/brakeman/checks/base_check.rb +13 -10
  6. data/lib/brakeman/checks/check_basic_auth.rb +2 -0
  7. data/lib/brakeman/checks/check_content_tag.rb +1 -2
  8. data/lib/brakeman/checks/check_csrf_token_forgery_cve.rb +28 -0
  9. data/lib/brakeman/checks/check_deserialize.rb +21 -1
  10. data/lib/brakeman/checks/check_execute.rb +40 -5
  11. data/lib/brakeman/checks/check_json_entity_escape.rb +38 -0
  12. data/lib/brakeman/checks/check_link_to.rb +1 -1
  13. data/lib/brakeman/checks/check_link_to_href.rb +1 -3
  14. data/lib/brakeman/checks/check_mass_assignment.rb +34 -4
  15. data/lib/brakeman/checks/check_model_attr_accessible.rb +1 -1
  16. data/lib/brakeman/checks/check_page_caching_cve.rb +37 -0
  17. data/lib/brakeman/checks/check_permit_attributes.rb +1 -1
  18. data/lib/brakeman/checks/check_skip_before_filter.rb +4 -4
  19. data/lib/brakeman/checks/check_sql.rb +24 -33
  20. data/lib/brakeman/checks/check_template_injection.rb +32 -0
  21. data/lib/brakeman/commandline.rb +25 -1
  22. data/lib/brakeman/differ.rb +0 -5
  23. data/lib/brakeman/options.rb +21 -1
  24. data/lib/brakeman/processor.rb +1 -1
  25. data/lib/brakeman/processors/alias_processor.rb +2 -3
  26. data/lib/brakeman/processors/lib/find_all_calls.rb +30 -14
  27. data/lib/brakeman/processors/lib/render_helper.rb +3 -1
  28. data/lib/brakeman/report.rb +4 -1
  29. data/lib/brakeman/report/ignore/config.rb +10 -2
  30. data/lib/brakeman/report/report_junit.rb +104 -0
  31. data/lib/brakeman/report/report_markdown.rb +0 -1
  32. data/lib/brakeman/report/report_text.rb +37 -16
  33. data/lib/brakeman/scanner.rb +4 -1
  34. data/lib/brakeman/tracker.rb +3 -1
  35. data/lib/brakeman/tracker/config.rb +4 -3
  36. data/lib/brakeman/tracker/constants.rb +8 -7
  37. data/lib/brakeman/util.rb +21 -3
  38. data/lib/brakeman/version.rb +1 -1
  39. data/lib/brakeman/warning_codes.rb +7 -0
  40. metadata +33 -8
@@ -8,7 +8,7 @@ require 'brakeman/checks/base_check'
8
8
  class Brakeman::CheckModelAttrAccessible < Brakeman::BaseCheck
9
9
  Brakeman::Checks.add self
10
10
 
11
- @description = "Reports models which have dangerous attributes defined under the attr_accessible whitelist."
11
+ @description = "Reports models which have dangerous attributes defined via attr_accessible"
12
12
 
13
13
  SUSP_ATTRS = [
14
14
  [:admin, :high], # Very dangerous unless some Rails authorization used
@@ -0,0 +1,37 @@
1
+ require 'brakeman/checks/base_check'
2
+
3
+ class Brakeman::CheckPageCachingCVE < Brakeman::BaseCheck
4
+ Brakeman::Checks.add self
5
+
6
+ @description = "Check for page caching vulnerability (CVE-2020-8159)"
7
+
8
+ def run_check
9
+ gem_name = 'actionpack-page_caching'
10
+ gem_version = tracker.config.gem_version(gem_name.to_sym)
11
+ upgrade_version = '1.2.2'
12
+ cve = 'CVE-2020-8159'
13
+
14
+ return unless gem_version and version_between?('0.0.0', '1.2.1', gem_version)
15
+
16
+ message = msg("Directory traversal vulnerability in ", msg_version(gem_version, gem_name), " ", msg_cve(cve), ". Upgrade to ", msg_version(upgrade_version, gem_name))
17
+
18
+ if uses_caches_page?
19
+ confidence = :high
20
+ else
21
+ confidence = :weak
22
+ end
23
+
24
+ warn :warning_type => 'Directory Traversal',
25
+ :warning_code => :CVE_2020_8159,
26
+ :message => message,
27
+ :confidence => confidence,
28
+ :link_path => 'https://groups.google.com/d/msg/rubyonrails-security/CFRVkEytdP8/c5gmICECAgAJ',
29
+ :gem_info => gemfile_or_environment(gem_name)
30
+ end
31
+
32
+ def uses_caches_page?
33
+ tracker.controllers.any? do |name, controller|
34
+ controller.options.has_key? :caches_page
35
+ end
36
+ end
37
+ end
@@ -3,7 +3,7 @@ require 'brakeman/checks/base_check'
3
3
  class Brakeman::CheckPermitAttributes < Brakeman::BaseCheck
4
4
  Brakeman::Checks.add self
5
5
 
6
- @description = "Warn on potentially dangerous attributes whitelisted via permit"
6
+ @description = "Warn on potentially dangerous attributes allowed via permit"
7
7
 
8
8
  SUSPICIOUS_KEYS = {
9
9
  admin: :high,
@@ -4,8 +4,8 @@ require 'brakeman/checks/base_check'
4
4
  #
5
5
  # skip_before_filter :verify_authenticity_token, :except => [...]
6
6
  #
7
- #which is essentially a blacklist approach (no actions are checked EXCEPT the
8
- #ones listed) versus a whitelist approach (ONLY the actions listed will skip
7
+ #which is essentially a skip-by-default approach (no actions are checked EXCEPT the
8
+ #ones listed) versus a enforce-by-default approach (ONLY the actions listed will skip
9
9
  #the check)
10
10
  class Brakeman::CheckSkipBeforeFilter < Brakeman::BaseCheck
11
11
  Brakeman::Checks.add self
@@ -26,7 +26,7 @@ class Brakeman::CheckSkipBeforeFilter < Brakeman::BaseCheck
26
26
  warn :class => controller.name, #ugh this should be a controller warning, too
27
27
  :warning_type => "Cross-Site Request Forgery",
28
28
  :warning_code => :csrf_blacklist,
29
- :message => msg("Use whitelist (", msg_code(":only => [..]"), ") when skipping CSRF check"),
29
+ :message => msg("List specific actions (", msg_code(":only => [..]"), ") when skipping CSRF check"),
30
30
  :code => filter,
31
31
  :confidence => :medium,
32
32
  :file => controller.file
@@ -35,7 +35,7 @@ class Brakeman::CheckSkipBeforeFilter < Brakeman::BaseCheck
35
35
  warn :controller => controller.name,
36
36
  :warning_code => :auth_blacklist,
37
37
  :warning_type => "Authentication",
38
- :message => msg("Use whitelist (", msg_code(":only => [..]"), ") when skipping authentication"),
38
+ :message => msg("List specific actions (", msg_code(":only => [..]"), ") when skipping authentication"),
39
39
  :code => filter,
40
40
  :confidence => :medium,
41
41
  :link_path => "authentication_whitelist",
@@ -71,32 +71,32 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
71
71
  def find_scope_calls
72
72
  scope_calls = []
73
73
 
74
- if version_between?("2.1.0", "3.0.9")
75
- ar_scope_calls(:named_scope) do |model, args|
76
- call = make_call(nil, :named_scope, args).line(args.line)
77
- scope_calls << scope_call_hash(call, model, :named_scope)
78
- end
79
- elsif version_between?("3.1.0", "9.9.9")
80
- ar_scope_calls(:scope) do |model, args|
81
- second_arg = args[2]
82
- next unless sexp? second_arg
83
-
84
- if second_arg.node_type == :iter and node_type? second_arg.block, :block, :call, :safe_call
85
- process_scope_with_block(model, args)
86
- elsif call? second_arg
87
- call = second_arg
88
- scope_calls << scope_call_hash(call, model, call.method)
89
- else
90
- call = make_call(nil, :scope, args).line(args.line)
91
- scope_calls << scope_call_hash(call, model, :scope)
92
- end
74
+ # Used in pre-3.1.0 versions of Rails
75
+ ar_scope_calls(:named_scope) do |model, args|
76
+ call = make_call(nil, :named_scope, args).line(args.line)
77
+ scope_calls << scope_call_hash(call, model, :named_scope)
78
+ end
79
+
80
+ # Use in 3.1.0 and later
81
+ ar_scope_calls(:scope) do |model, args|
82
+ second_arg = args[2]
83
+ next unless sexp? second_arg
84
+
85
+ if second_arg.node_type == :iter and node_type? second_arg.block, :block, :call, :safe_call
86
+ process_scope_with_block(model, args)
87
+ elsif call? second_arg
88
+ call = second_arg
89
+ scope_calls << scope_call_hash(call, model, call.method)
90
+ else
91
+ call = make_call(nil, :scope, args).line(args.line)
92
+ scope_calls << scope_call_hash(call, model, :scope)
93
93
  end
94
94
  end
95
95
 
96
96
  scope_calls
97
97
  end
98
98
 
99
- def ar_scope_calls(symbol_name = :named_scope, &block)
99
+ def ar_scope_calls(symbol_name, &block)
100
100
  active_record_models.each do |name, model|
101
101
  model_args = model.options[symbol_name]
102
102
  if model_args
@@ -393,6 +393,8 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
393
393
  nil
394
394
  end
395
395
 
396
+ TO_STRING_METHODS = [:to_s, :squish, :strip, :strip_heredoc]
397
+
396
398
  #Returns value if interpolated value is not something safe
397
399
  def unsafe_string_interp? exp
398
400
  if node_type? exp, :evstr
@@ -403,7 +405,7 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
403
405
 
404
406
  if not sexp? value
405
407
  nil
406
- elsif call? value and value.method == :to_s
408
+ elsif call? value and TO_STRING_METHODS.include? value.method
407
409
  unsafe_string_interp? value.target
408
410
  elsif call? value and safe_literal_target? value
409
411
  nil
@@ -466,7 +468,7 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
466
468
  unless IGNORE_METHODS_IN_SQL.include? exp.method
467
469
  if has_immediate_user_input? exp
468
470
  exp
469
- elsif exp.method == :to_s
471
+ elsif TO_STRING_METHODS.include? exp.method
470
472
  find_dangerous_value exp.target, ignore_hash
471
473
  else
472
474
  check_call exp
@@ -523,8 +525,6 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
523
525
  false
524
526
  end
525
527
 
526
- STRING_METHODS = Set[:<<, :+, :concat, :prepend]
527
-
528
528
  def check_for_string_building exp
529
529
  return unless call? exp
530
530
 
@@ -571,15 +571,6 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
571
571
  end
572
572
  end
573
573
 
574
- def string_building? exp
575
- return false unless call? exp and STRING_METHODS.include? exp.method
576
-
577
- node_type? exp.target, :str, :dstr or
578
- node_type? exp.first_arg, :str, :dstr or
579
- string_building? exp.target or
580
- string_building? exp.first_arg
581
- end
582
-
583
574
  IGNORE_METHODS_IN_SQL = Set[:id, :merge_conditions, :table_name, :quoted_table_name,
584
575
  :quoted_primary_key, :to_i, :to_f, :sanitize_sql, :sanitize_sql_array,
585
576
  :sanitize_sql_for_assignment, :sanitize_sql_for_conditions, :sanitize_sql_hash,
@@ -0,0 +1,32 @@
1
+ require 'brakeman/checks/base_check'
2
+
3
+ class Brakeman::CheckTemplateInjection < Brakeman::BaseCheck
4
+ Brakeman::Checks.add self
5
+
6
+ @description = "Searches for evaluation of user input through template injection"
7
+
8
+ #Process calls
9
+ def run_check
10
+ Brakeman.debug "Finding ERB.new calls"
11
+ erb_calls = tracker.find_call :target => :ERB, :method => :new, :nested => true
12
+
13
+ Brakeman.debug "Processing ERB.new calls"
14
+ erb_calls.each do |call|
15
+ process_result call
16
+ end
17
+ end
18
+
19
+ #Warns if eval includes user input
20
+ def process_result result
21
+ return unless original? result
22
+
23
+ if input = include_user_input?(result[:call].arglist)
24
+ warn :result => result,
25
+ :warning_type => "Template Injection",
26
+ :warning_code => :erb_template_injection,
27
+ :message => msg(msg_input(input), " used directly in ", msg_code("ERB"), " template, which might enable remote code execution"),
28
+ :user_input => input,
29
+ :confidence => :high
30
+ end
31
+ end
32
+ end
@@ -102,6 +102,13 @@ module Brakeman
102
102
  app_path = "."
103
103
  end
104
104
 
105
+ if options[:ensure_ignore_notes] and options[:previous_results_json]
106
+ warn '[Notice] --ensure-ignore-notes may not be used at the same ' \
107
+ 'time as --compare. Deactivating --ensure-ignore-notes. ' \
108
+ 'Please see `brakeman --help` for valid options'
109
+ options[:ensure_ignore_notes] = false
110
+ end
111
+
105
112
  return options, app_path
106
113
  end
107
114
 
@@ -115,7 +122,20 @@ module Brakeman
115
122
 
116
123
  # Runs a regular report based on the options provided.
117
124
  def regular_report options
118
- tracker = run_brakeman options
125
+ tracker = run_brakeman options
126
+
127
+ ensure_ignore_notes_failed = false
128
+ if tracker.options[:ensure_ignore_notes]
129
+ fingerprints = Brakeman::ignore_file_entries_with_empty_notes tracker.ignored_filter&.file
130
+
131
+ unless fingerprints.empty?
132
+ ensure_ignore_notes_failed = true
133
+ warn '[Error] Notes required for all ignored warnings when ' \
134
+ '--ensure-ignore-notes is set. No notes provided for these ' \
135
+ 'warnings: '
136
+ fingerprints.each { |f| warn f }
137
+ end
138
+ end
119
139
 
120
140
  if tracker.options[:exit_on_warn] and not tracker.filtered_warnings.empty?
121
141
  quit Brakeman::Warnings_Found_Exit_Code
@@ -124,6 +144,10 @@ module Brakeman
124
144
  if tracker.options[:exit_on_error] and tracker.errors.any?
125
145
  quit Brakeman::Errors_Found_Exit_Code
126
146
  end
147
+
148
+ if ensure_ignore_notes_failed
149
+ quit Brakeman::Empty_Ignore_Note_Exit_Code
150
+ end
127
151
  end
128
152
 
129
153
  # Actually run Brakeman.
@@ -1,8 +1,6 @@
1
1
  # extracting the diff logic to it's own class for consistency. Currently handles
2
2
  # an array of Brakeman::Warnings or plain hash representations.
3
3
  class Brakeman::Differ
4
- DEFAULT_HASH = {:new => [], :fixed => []}
5
- OLD_WARNING_KEYS = [:warning_type, :location, :code, :message, :file, :link, :confidence, :user_input]
6
4
  attr_reader :old_warnings, :new_warnings
7
5
 
8
6
  def initialize new_warnings, old_warnings
@@ -11,9 +9,6 @@ class Brakeman::Differ
11
9
  end
12
10
 
13
11
  def diff
14
- # get the type of elements
15
- return DEFAULT_HASH if @new_warnings.empty?
16
-
17
12
  warnings = {}
18
13
  warnings[:new] = @new_warnings - @old_warnings
19
14
  warnings[:fixed] = @old_warnings - @new_warnings
@@ -67,6 +67,10 @@ module Brakeman::Options
67
67
  options[:ensure_latest] = true
68
68
  end
69
69
 
70
+ opts.on "--ensure-ignore-notes", "Fail when an ignored warnings does not include a note" do
71
+ options[:ensure_ignore_notes] = true
72
+ end
73
+
70
74
  opts.on "-3", "--rails3", "Force Rails 3 mode" do
71
75
  options[:rails3] = true
72
76
  end
@@ -225,7 +229,7 @@ module Brakeman::Options
225
229
 
226
230
  opts.on "-f",
227
231
  "--format TYPE",
228
- [:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc, :plain, :table],
232
+ [:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc, :plain, :table, :junit],
229
233
  "Specify output formats. Default is text" do |type|
230
234
 
231
235
  type = "s" if type == :text
@@ -301,6 +305,22 @@ module Brakeman::Options
301
305
  options[:github_repo] = repo
302
306
  end
303
307
 
308
+ opts.on "--text-fields field1,field2,etc.", Array, "Specify fields for text report format" do |format|
309
+ valid_options = [:category, :category_id, :check, :code, :confidence, :file, :fingerprint, :line, :link, :message, :render_path]
310
+
311
+ options[:text_fields] = format.map(&:to_sym)
312
+
313
+ if options[:text_fields] == [:all]
314
+ options[:text_fields] = valid_options
315
+ else
316
+ invalid_options = (options[:text_fields] - valid_options)
317
+
318
+ unless invalid_options.empty?
319
+ raise OptionParser::ParseError, "\nInvalid format options: #{invalid_options.inspect}"
320
+ end
321
+ end
322
+ end
323
+
304
324
  opts.on "-w",
305
325
  "--confidence-level LEVEL",
306
326
  ["1", "2", "3"],
@@ -53,7 +53,7 @@ module Brakeman
53
53
  #Process a model source
54
54
  def process_model src, file_name
55
55
  result = ModelProcessor.new(@tracker).process_model src, file_name
56
- AliasProcessor.new(@tracker).process result if result
56
+ AliasProcessor.new(@tracker, file_name).process result if result
57
57
  end
58
58
 
59
59
  #Process either an ERB or HAML template
@@ -82,7 +82,6 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
82
82
  def replace exp, int = 0
83
83
  return exp if int > 3
84
84
 
85
-
86
85
  if replacement = env[exp] and not duplicate? replacement
87
86
  replace(replacement.deep_clone(exp.line), int + 1)
88
87
  elsif tracker and replacement = tracker.constant_lookup(exp) and not duplicate? replacement
@@ -731,14 +730,14 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
731
730
  def array_include_all_literals? exp
732
731
  call? exp and
733
732
  exp.method == :include? and
734
- all_literals? exp.target
733
+ (all_literals? exp.target or dir_glob? exp.target)
735
734
  end
736
735
 
737
736
  def array_detect_all_literals? exp
738
737
  call? exp and
739
738
  [:detect, :find].include? exp.method and
740
739
  exp.first_arg.nil? and
741
- all_literals? exp.target
740
+ (all_literals? exp.target or dir_glob? exp.target)
742
741
  end
743
742
 
744
743
  #Sets @inside_if = true
@@ -20,6 +20,7 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
20
20
  @current_template = opts[:template]
21
21
  @current_file = opts[:file]
22
22
  @current_call = nil
23
+ @full_call = nil
23
24
  process exp
24
25
  end
25
26
 
@@ -60,7 +61,7 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
60
61
  end
61
62
 
62
63
  def process_call exp
63
- @calls << create_call_hash(exp)
64
+ @calls << create_call_hash(exp).freeze
64
65
  exp
65
66
  end
66
67
 
@@ -72,6 +73,7 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
72
73
 
73
74
  call_hash[:block] = exp.block
74
75
  call_hash[:block_args] = exp.block_args
76
+ call_hash.freeze
75
77
 
76
78
  @calls << call_hash
77
79
 
@@ -88,7 +90,7 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
88
90
  #Calls to render() are converted to s(:render, ...) but we would
89
91
  #like them in the call cache still for speed
90
92
  def process_render exp
91
- process exp.last if sexp? exp.last
93
+ process_all exp
92
94
 
93
95
  add_simple_call :render, exp
94
96
 
@@ -136,7 +138,8 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
136
138
  :call => exp,
137
139
  :nested => false,
138
140
  :location => make_location,
139
- :parent => @current_call }
141
+ :parent => @current_call,
142
+ :full_call => @full_call }.freeze
140
143
  end
141
144
 
142
145
  #Gets the target of a call as a Symbol
@@ -213,34 +216,47 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
213
216
  #Return info hash for a call Sexp
214
217
  def create_call_hash exp
215
218
  target = get_target exp.target
216
-
217
- if call? target or node_type? target, :dxstr # need to index `` even if target of a call
218
- already_in_target = @in_target
219
- @in_target = true
220
- process target
221
- @in_target = already_in_target
222
-
223
- target = get_target(target, :include_calls)
224
- end
219
+ target_symbol = get_target(target, :include_calls)
225
220
 
226
221
  method = exp.method
227
222
 
228
223
  call_hash = {
229
- :target => target,
224
+ :target => target_symbol,
230
225
  :method => method,
231
226
  :call => exp,
232
227
  :nested => @in_target,
233
228
  :chain => get_chain(exp),
234
229
  :location => make_location,
235
- :parent => @current_call
230
+ :parent => @current_call,
231
+ :full_call => @full_call
236
232
  }
237
233
 
234
+ unless @in_target
235
+ @full_call = call_hash
236
+ end
237
+
238
+ # Process up the call chain
239
+ if call? target or node_type? target, :dxstr # need to index `` even if target of a call
240
+ already_in_target = @in_target
241
+ @in_target = true
242
+ process target
243
+ @in_target = already_in_target
244
+ end
245
+
246
+ # Process call arguments
247
+ # but add the current call as the 'parent'
248
+ # to any calls in the arguments
238
249
  old_parent = @current_call
239
250
  @current_call = call_hash
240
251
 
252
+ # Do not set @full_call when processing arguments
253
+ old_full_call = @full_call
254
+ @full_call = nil
255
+
241
256
  process_call_args exp
242
257
 
243
258
  @current_call = old_parent
259
+ @full_call = old_full_call
244
260
 
245
261
  call_hash
246
262
  end