brakeman-lib 4.7.1 → 4.9.0

Sign up to get free protection for your applications and to get access to all the features.
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