brakeman-lib 4.5.1 → 4.7.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +158 -109
  3. data/README.md +1 -2
  4. data/lib/brakeman/call_index.rb +54 -15
  5. data/lib/brakeman/checks/base_check.rb +50 -47
  6. data/lib/brakeman/checks/check_cookie_serialization.rb +22 -0
  7. data/lib/brakeman/checks/check_cross_site_scripting.rb +4 -4
  8. data/lib/brakeman/checks/check_deserialize.rb +3 -6
  9. data/lib/brakeman/checks/check_execute.rb +26 -1
  10. data/lib/brakeman/checks/check_file_access.rb +7 -1
  11. data/lib/brakeman/checks/check_header_dos.rb +2 -2
  12. data/lib/brakeman/checks/check_i18n_xss.rb +2 -2
  13. data/lib/brakeman/checks/check_jruby_xml.rb +2 -2
  14. data/lib/brakeman/checks/check_json_parsing.rb +2 -2
  15. data/lib/brakeman/checks/check_mass_assignment.rb +1 -1
  16. data/lib/brakeman/checks/check_mime_type_dos.rb +2 -2
  17. data/lib/brakeman/checks/check_nested_attributes_bypass.rb +1 -1
  18. data/lib/brakeman/checks/check_reverse_tabnabbing.rb +58 -0
  19. data/lib/brakeman/checks/check_sanitize_methods.rb +2 -2
  20. data/lib/brakeman/checks/check_session_settings.rb +5 -2
  21. data/lib/brakeman/checks/check_sql.rb +24 -22
  22. data/lib/brakeman/checks/check_xml_dos.rb +2 -2
  23. data/lib/brakeman/checks/check_yaml_parsing.rb +10 -18
  24. data/lib/brakeman/differ.rb +16 -28
  25. data/lib/brakeman/file_parser.rb +4 -8
  26. data/lib/brakeman/file_path.rb +14 -0
  27. data/lib/brakeman/parsers/haml_embedded.rb +1 -1
  28. data/lib/brakeman/parsers/template_parser.rb +3 -1
  29. data/lib/brakeman/processor.rb +2 -2
  30. data/lib/brakeman/processors/alias_processor.rb +15 -1
  31. data/lib/brakeman/processors/base_processor.rb +2 -0
  32. data/lib/brakeman/processors/controller_processor.rb +4 -4
  33. data/lib/brakeman/processors/gem_processor.rb +10 -2
  34. data/lib/brakeman/processors/haml_template_processor.rb +87 -123
  35. data/lib/brakeman/processors/lib/call_conversion_helper.rb +5 -4
  36. data/lib/brakeman/processors/lib/find_all_calls.rb +27 -4
  37. data/lib/brakeman/processors/lib/find_call.rb +3 -64
  38. data/lib/brakeman/processors/lib/rails2_config_processor.rb +1 -1
  39. data/lib/brakeman/processors/template_alias_processor.rb +28 -0
  40. data/lib/brakeman/processors/template_processor.rb +10 -6
  41. data/lib/brakeman/report/report_text.rb +4 -5
  42. data/lib/brakeman/rescanner.rb +4 -0
  43. data/lib/brakeman/tracker.rb +26 -2
  44. data/lib/brakeman/tracker/config.rb +38 -73
  45. data/lib/brakeman/tracker/constants.rb +2 -1
  46. data/lib/brakeman/util.rb +5 -3
  47. data/lib/brakeman/version.rb +1 -1
  48. data/lib/brakeman/warning.rb +4 -0
  49. data/lib/brakeman/warning_codes.rb +3 -0
  50. data/lib/ruby_parser/bm_sexp.rb +7 -2
  51. metadata +18 -17
@@ -25,7 +25,7 @@ class Brakeman::CheckHeaderDoS < Brakeman::BaseCheck
25
25
  end
26
26
 
27
27
  def has_workaround?
28
- tracker.check_initializers(:ActiveSupport, :on_load).any? and
29
- tracker.check_initializers(:"ActionView::LookupContext::DetailsKey", :class_eval).any?
28
+ tracker.find_call(target: :ActiveSupport, method: :on_load).any? and
29
+ tracker.find_call(target: :"ActionView::LookupContext::DetailsKey", method: :class_eval).any?
30
30
  end
31
31
  end
@@ -41,8 +41,8 @@ class Brakeman::CheckI18nXSS < Brakeman::BaseCheck
41
41
  end
42
42
 
43
43
  def has_workaround?
44
- tracker.check_initializers(:I18n, :const_defined?).any? do |match|
45
- match.last.first_arg == s(:lit, :MissingTranslation)
44
+ tracker.find_call(target: :I18n, method: :const_defined?, chained: true).any? do |match|
45
+ match[:call].first_arg == s(:lit, :MissingTranslation)
46
46
  end
47
47
  end
48
48
  end
@@ -20,8 +20,8 @@ class Brakeman::CheckJRubyXML < Brakeman::BaseCheck
20
20
  end
21
21
 
22
22
  #Check for workaround
23
- tracker.check_initializers(:"ActiveSupport::XmlMini", :backend=).each do |result|
24
- arg = result.call.first_arg
23
+ tracker.find_call(target: :"ActiveSupport::XmlMini", method: :backend=, chained: true).each do |result|
24
+ arg = result[:call].first_arg
25
25
 
26
26
  return if string? arg and arg.value == "REXML"
27
27
  end
@@ -44,13 +44,13 @@ class Brakeman::CheckJSONParsing < Brakeman::BaseCheck
44
44
 
45
45
  #Check for `ActiveSupport::JSON.backend = "JSONGem"`
46
46
  def uses_gem_backend?
47
- matches = tracker.check_initializers(:'ActiveSupport::JSON', :backend=)
47
+ matches = tracker.find_call(target: :'ActiveSupport::JSON', method: :backend=, chained: true)
48
48
 
49
49
  unless matches.empty?
50
50
  json_gem = s(:str, "JSONGem")
51
51
 
52
52
  matches.each do |result|
53
- if result.call.first_arg == json_gem
53
+ if result[:call].first_arg == json_gem
54
54
  return true
55
55
  end
56
56
  end
@@ -158,7 +158,7 @@ class Brakeman::CheckMassAssignment < Brakeman::BaseCheck
158
158
 
159
159
  # Look for and warn about uses of Parameters#permit! for mass assignment
160
160
  def check_permit!
161
- tracker.find_call(:method => :permit!).each do |result|
161
+ tracker.find_call(:method => :permit!, :nested => true).each do |result|
162
162
  if params? result[:call].target and not result[:chain].include? :slice
163
163
  warn_on_permit! result
164
164
  end
@@ -30,8 +30,8 @@ class Brakeman::CheckMimeTypeDoS < Brakeman::BaseCheck
30
30
  end
31
31
 
32
32
  def has_workaround?
33
- tracker.check_initializers(:Mime, :const_set).any? do |match|
34
- arg = match.call.first_arg
33
+ tracker.find_call(target: :Mime, method: :const_set).any? do |match|
34
+ arg = match[:call].first_arg
35
35
 
36
36
  symbol? arg and arg.value == :LOOKUP
37
37
  end
@@ -53,6 +53,6 @@ class Brakeman::CheckNestedAttributesBypass < Brakeman::BaseCheck
53
53
  end
54
54
 
55
55
  def workaround?
56
- tracker.check_initializers([], :will_be_destroyed?).any?
56
+ tracker.find_call(method: :will_be_destroyed?).any?
57
57
  end
58
58
  end
@@ -0,0 +1,58 @@
1
+ require 'brakeman/checks/base_check'
2
+
3
+ class Brakeman::CheckReverseTabnabbing < Brakeman::BaseCheck
4
+ Brakeman::Checks.add_optional self
5
+
6
+ @description = "Checks for reverse tabnabbing cases on 'link_to' calls"
7
+
8
+ def run_check
9
+ calls = tracker.find_call :methods => :link_to
10
+ calls.each do |call|
11
+ process_result call
12
+ end
13
+ end
14
+
15
+ def process_result result
16
+ return unless original? result and result[:call].last_arg
17
+
18
+ html_opts = result[:call].last_arg
19
+ return unless hash? html_opts
20
+
21
+ target = hash_access html_opts, :target
22
+ unless target &&
23
+ (string?(target) && target.value == "_blank" ||
24
+ symbol?(target) && target.value == :_blank)
25
+ return
26
+ end
27
+
28
+ target_url = result[:block] ? result[:call].first_arg : result[:call].second_arg
29
+
30
+ # `url_for` and `_path` calls lead to urls on to the same origin.
31
+ # That means that an adversary would need to run javascript on
32
+ # the victim application's domain. If that is the case, the adversary
33
+ # already has the ability to redirect the victim user anywhere.
34
+ # Also statically provided URLs (interpolated or otherwise) are also
35
+ # ignored as they produce many false positives.
36
+ return if !call?(target_url) || target_url.method.match(/^url_for$|_path$/)
37
+
38
+ rel = hash_access html_opts, :rel
39
+ confidence = :medium
40
+
41
+ if rel && string?(rel) then
42
+ rel_opt = rel.value
43
+ return if rel_opt.include?("noopener") && rel_opt.include?("noreferrer")
44
+
45
+ if rel_opt.include?("noopener") ^ rel_opt.include?("noreferrer") then
46
+ confidence = :weak
47
+ end
48
+ end
49
+
50
+ warn :result => result,
51
+ :warning_type => "Reverse Tabnabbing",
52
+ :warning_code => :reverse_tabnabbing,
53
+ :message => msg("When opening a link in a new tab without setting ", msg_code('rel: "noopener noreferrer"'),
54
+ ", the new tab can control the parent tab's location. For example, an attacker could redirect to a phishing page."),
55
+ :confidence => confidence,
56
+ :user_input => rel
57
+ end
58
+ end
@@ -70,7 +70,7 @@ class Brakeman::CheckSanitizeMethods < Brakeman::BaseCheck
70
70
 
71
71
  def check_cve_2018_8048
72
72
  if loofah_vulnerable_cve_2018_8048?
73
- message = msg(msg_version(tracker.config.gem_version(:loofah), "loofah gem"), " is vulnerable (CVE-2018-8048). Upgrade to 2.1.2")
73
+ message = msg(msg_version(tracker.config.gem_version(:loofah), "loofah gem"), " is vulnerable (CVE-2018-8048). Upgrade to 2.2.1")
74
74
 
75
75
  if tracker.find_call(:target => false, :method => :sanitize).any?
76
76
  confidence = :high
@@ -90,7 +90,7 @@ class Brakeman::CheckSanitizeMethods < Brakeman::BaseCheck
90
90
  def loofah_vulnerable_cve_2018_8048?
91
91
  loofah_version = tracker.config.gem_version(:loofah)
92
92
 
93
- loofah_version and loofah_version < "2.1.2"
93
+ loofah_version and loofah_version < "2.2.1"
94
94
  end
95
95
 
96
96
  def warn_sanitizer_cve cve, link, upgrade_version
@@ -21,8 +21,11 @@ class Brakeman::CheckSessionSettings < Brakeman::BaseCheck
21
21
 
22
22
  check_for_issues settings, @app_tree.file_path("config/environment.rb")
23
23
 
24
- ["session_store.rb", "secret_token.rb"].each do |file|
25
- if tracker.initializers[file] and not ignored? file
24
+ session_store = @app_tree.file_path("config/initializers/session_store.rb")
25
+ secret_token = @app_tree.file_path("config/initializers/secret_token.rb")
26
+
27
+ [session_store, secret_token].each do |file|
28
+ if tracker.initializers[file] and not ignored? file.basename
26
29
  process tracker.initializers[file]
27
30
  end
28
31
  end
@@ -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, :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
@@ -34,8 +34,8 @@ class Brakeman::CheckXMLDoS < Brakeman::BaseCheck
34
34
  end
35
35
 
36
36
  def has_workaround?
37
- tracker.check_initializers(:"ActiveSupport::XmlMini", :backend=).any? do |match|
38
- arg = match.call.first_arg
37
+ tracker.find_call(target: :"ActiveSupport::XmlMini", method: :backend=).any? do |match|
38
+ arg = match[:call].first_arg
39
39
  if string? arg
40
40
  value = arg.value
41
41
  value == 'Nokogiri' or value == 'LibXML'
@@ -48,21 +48,17 @@ class Brakeman::CheckYAMLParsing < Brakeman::BaseCheck
48
48
  def disabled_xml_parser?
49
49
  if version_between? "0.0.0", "2.3.14"
50
50
  #Look for ActionController::Base.param_parsers.delete(Mime::XML)
51
- params_parser = s(:call,
52
- s(:colon2, s(:const, :ActionController), :Base),
53
- :param_parsers)
54
-
55
- matches = tracker.check_initializers(params_parser, :delete)
51
+ matches = tracker.find_call(target: :"ActionController::Base.param_parsers", method: :delete)
56
52
  else
57
53
  #Look for ActionDispatch::ParamsParser::DEFAULT_PARSERS.delete(Mime::XML)
58
- matches = tracker.check_initializers(:"ActionDispatch::ParamsParser::DEFAULT_PARSERS", :delete)
54
+ matches = tracker.find_call(target: :"ActionDispatch::ParamsParser::DEFAULT_PARSERS", method: :delete)
59
55
  end
60
56
 
61
57
  unless matches.empty?
62
58
  mime_xml = s(:colon2, s(:const, :Mime), :XML)
63
59
 
64
60
  matches.each do |result|
65
- if result.call.first_arg == mime_xml
61
+ if result[:call].first_arg == mime_xml
66
62
  return true
67
63
  end
68
64
  end
@@ -74,18 +70,14 @@ class Brakeman::CheckYAMLParsing < Brakeman::BaseCheck
74
70
  #Look for ActionController::Base.param_parsers[Mime::YAML] = :yaml
75
71
  #in Rails 2.x apps
76
72
  def enabled_yaml_parser?
77
- param_parsers = s(:call,
78
- s(:colon2, s(:const, :ActionController), :Base),
79
- :param_parsers)
80
-
81
- matches = tracker.check_initializers(param_parsers, :[]=)
73
+ matches = tracker.find_call(target: :'ActionController::Base.param_parsers', method: :[]=)
82
74
 
83
75
  mime_yaml = s(:colon2, s(:const, :Mime), :YAML)
84
76
 
85
77
  matches.each do |result|
86
- if result.call.first_arg == mime_yaml and
87
- symbol? result.call.second_arg and
88
- result.call.second_arg.value == :yaml
78
+ if result[:call].first_arg == mime_yaml and
79
+ symbol? result[:call].second_arg and
80
+ result[:call].second_arg.value == :yaml
89
81
 
90
82
  return true
91
83
  end
@@ -96,16 +88,16 @@ class Brakeman::CheckYAMLParsing < Brakeman::BaseCheck
96
88
 
97
89
  def disabled_xml_dangerous_types?
98
90
  if version_between? "0.0.0", "2.3.14"
99
- matches = tracker.check_initializers(:"ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING", :delete)
91
+ matches = tracker.find_call(target: :"ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING", method: :delete)
100
92
  else
101
- matches = tracker.check_initializers(:"ActiveSupport::XmlMini::PARSING", :delete)
93
+ matches = tracker.find_call(target: :"ActiveSupport::XmlMini::PARSING", method: :delete)
102
94
  end
103
95
 
104
96
  symbols_off = false
105
97
  yaml_off = false
106
98
 
107
99
  matches.each do |result|
108
- arg = result.call.first_arg
100
+ arg = result[:call].first_arg
109
101
 
110
102
  if string? arg
111
103
  if arg.value == "yaml"
@@ -24,43 +24,31 @@ class Brakeman::Differ
24
24
  # second pass to cleanup any vulns which have changed in line number only.
25
25
  # Given a list of new warnings, delete pairs of new/fixed vulns that differ
26
26
  # only by line number.
27
- # Horrible O(n^2) performance. Keep n small :-/
28
27
  def second_pass(warnings)
29
- # keep track of the number of elements deleted because the index numbers
30
- # won't update as the list is modified
31
- elements_deleted_offset = 0
28
+ new_fingerprints = Set.new(warnings[:new].map(&method(:fingerprint)))
29
+ fixed_fingerprints = Set.new(warnings[:fixed].map(&method(:fingerprint)))
32
30
 
33
- # dup this list since we will be deleting from it and the iterator gets confused.
34
- # use _with_index for fast deletion as opposed to .reject!{|obj| obj == *_warning}
35
- warnings[:new].dup.each_with_index do |new_warning, new_warning_id|
36
- warnings[:fixed].each_with_index do |fixed_warning, fixed_warning_id|
37
- if eql_except_line_number new_warning, fixed_warning
38
- warnings[:new].delete_at(new_warning_id - elements_deleted_offset)
39
- elements_deleted_offset += 1
40
- warnings[:fixed].delete_at(fixed_warning_id)
41
- break
42
- end
31
+ # Remove warnings which fingerprints are both in :new and :fixed
32
+ shared_fingerprints = new_fingerprints.intersection(fixed_fingerprints)
33
+
34
+ unless shared_fingerprints.empty?
35
+ warnings[:new].delete_if do |warning|
36
+ shared_fingerprints.include?(fingerprint(warning))
37
+ end
38
+
39
+ warnings[:fixed].delete_if do |warning|
40
+ shared_fingerprints.include?(fingerprint(warning))
43
41
  end
44
42
  end
45
43
 
46
44
  warnings
47
45
  end
48
46
 
49
- def eql_except_line_number new_warning, fixed_warning
50
- # can't do this ahead of time, as callers may be expecting a Brakeman::Warning
51
- if new_warning.is_a? Brakeman::Warning
52
- new_warning = new_warning.to_hash
53
- fixed_warning = fixed_warning.to_hash
54
- end
55
-
56
- if new_warning[:fingerprint] and fixed_warning[:fingerprint]
57
- new_warning[:fingerprint] == fixed_warning[:fingerprint]
47
+ def fingerprint(warning)
48
+ if warning.is_a?(Brakeman::Warning)
49
+ warning.fingerprint
58
50
  else
59
- OLD_WARNING_KEYS.each do |attr|
60
- return false if new_warning[attr] != fixed_warning[attr]
61
- end
62
-
63
- true
51
+ warning[:fingerprint]
64
52
  end
65
53
  end
66
54
  end
@@ -33,17 +33,13 @@ module Brakeman
33
33
  end
34
34
  end
35
35
 
36
- def parse_ruby input, path, parser = RubyParser.new
36
+ def parse_ruby input, path
37
37
  begin
38
38
  Brakeman.debug "Parsing #{path}"
39
- parser.parse input, path, @timeout
39
+ RubyParser.new.parse input, path, @timeout
40
40
  rescue Racc::ParseError => e
41
- if parser.class == RubyParser
42
- return parse_ruby(input, path, RubyParser.latest)
43
- else
44
- @tracker.error e, "Could not parse #{path}"
45
- nil
46
- end
41
+ @tracker.error e, "Could not parse #{path}"
42
+ nil
47
43
  rescue Timeout::Error => e
48
44
  @tracker.error Exception.new("Parsing #{path} took too long (> #{@timeout} seconds). Try increasing the limit with --parser-timeout"), caller
49
45
  nil
@@ -35,6 +35,11 @@ module Brakeman
35
35
  @relative = relative_path
36
36
  end
37
37
 
38
+ # Just the file name, no path
39
+ def basename
40
+ @basename ||= File.basename(self.relative)
41
+ end
42
+
38
43
  # Read file from absolute path.
39
44
  def read
40
45
  File.read self.absolute
@@ -67,5 +72,14 @@ module Brakeman
67
72
  def to_s
68
73
  self.to_str
69
74
  end
75
+
76
+ def hash
77
+ @hash ||= [@absolute, @relative].hash
78
+ end
79
+
80
+ def eql? rhs
81
+ @absolute == rhs.absolute and
82
+ @relative == rhs.relative
83
+ end
70
84
  end
71
85
  end