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.
- checksums.yaml +4 -4
- data/CHANGES.md +158 -109
- data/README.md +1 -2
- data/lib/brakeman/call_index.rb +54 -15
- data/lib/brakeman/checks/base_check.rb +50 -47
- data/lib/brakeman/checks/check_cookie_serialization.rb +22 -0
- data/lib/brakeman/checks/check_cross_site_scripting.rb +4 -4
- data/lib/brakeman/checks/check_deserialize.rb +3 -6
- data/lib/brakeman/checks/check_execute.rb +26 -1
- data/lib/brakeman/checks/check_file_access.rb +7 -1
- data/lib/brakeman/checks/check_header_dos.rb +2 -2
- data/lib/brakeman/checks/check_i18n_xss.rb +2 -2
- data/lib/brakeman/checks/check_jruby_xml.rb +2 -2
- data/lib/brakeman/checks/check_json_parsing.rb +2 -2
- data/lib/brakeman/checks/check_mass_assignment.rb +1 -1
- data/lib/brakeman/checks/check_mime_type_dos.rb +2 -2
- data/lib/brakeman/checks/check_nested_attributes_bypass.rb +1 -1
- data/lib/brakeman/checks/check_reverse_tabnabbing.rb +58 -0
- data/lib/brakeman/checks/check_sanitize_methods.rb +2 -2
- data/lib/brakeman/checks/check_session_settings.rb +5 -2
- data/lib/brakeman/checks/check_sql.rb +24 -22
- data/lib/brakeman/checks/check_xml_dos.rb +2 -2
- data/lib/brakeman/checks/check_yaml_parsing.rb +10 -18
- data/lib/brakeman/differ.rb +16 -28
- data/lib/brakeman/file_parser.rb +4 -8
- data/lib/brakeman/file_path.rb +14 -0
- data/lib/brakeman/parsers/haml_embedded.rb +1 -1
- data/lib/brakeman/parsers/template_parser.rb +3 -1
- data/lib/brakeman/processor.rb +2 -2
- data/lib/brakeman/processors/alias_processor.rb +15 -1
- data/lib/brakeman/processors/base_processor.rb +2 -0
- data/lib/brakeman/processors/controller_processor.rb +4 -4
- data/lib/brakeman/processors/gem_processor.rb +10 -2
- data/lib/brakeman/processors/haml_template_processor.rb +87 -123
- data/lib/brakeman/processors/lib/call_conversion_helper.rb +5 -4
- data/lib/brakeman/processors/lib/find_all_calls.rb +27 -4
- data/lib/brakeman/processors/lib/find_call.rb +3 -64
- data/lib/brakeman/processors/lib/rails2_config_processor.rb +1 -1
- data/lib/brakeman/processors/template_alias_processor.rb +28 -0
- data/lib/brakeman/processors/template_processor.rb +10 -6
- data/lib/brakeman/report/report_text.rb +4 -5
- data/lib/brakeman/rescanner.rb +4 -0
- data/lib/brakeman/tracker.rb +26 -2
- data/lib/brakeman/tracker/config.rb +38 -73
- data/lib/brakeman/tracker/constants.rb +2 -1
- data/lib/brakeman/util.rb +5 -3
- data/lib/brakeman/version.rb +1 -1
- data/lib/brakeman/warning.rb +4 -0
- data/lib/brakeman/warning_codes.rb +3 -0
- data/lib/ruby_parser/bm_sexp.rb +7 -2
- metadata +18 -17
@@ -25,7 +25,7 @@ class Brakeman::CheckHeaderDoS < Brakeman::BaseCheck
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def has_workaround?
|
28
|
-
tracker.
|
29
|
-
|
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.
|
45
|
-
match.
|
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.
|
24
|
-
arg = result
|
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.
|
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
|
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
|
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.
|
34
|
-
arg = match
|
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
|
@@ -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
|
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
|
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
|
-
|
25
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
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
|
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
|
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.
|
38
|
-
arg = match
|
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
|
-
|
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.
|
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
|
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
|
-
|
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
|
87
|
-
symbol? result
|
88
|
-
result
|
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.
|
91
|
+
matches = tracker.find_call(target: :"ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING", method: :delete)
|
100
92
|
else
|
101
|
-
matches = tracker.
|
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
|
100
|
+
arg = result[:call].first_arg
|
109
101
|
|
110
102
|
if string? arg
|
111
103
|
if arg.value == "yaml"
|
data/lib/brakeman/differ.rb
CHANGED
@@ -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
|
-
|
30
|
-
|
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
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
50
|
-
|
51
|
-
|
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
|
-
|
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
|
data/lib/brakeman/file_parser.rb
CHANGED
@@ -33,17 +33,13 @@ module Brakeman
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def parse_ruby input, path
|
36
|
+
def parse_ruby input, path
|
37
37
|
begin
|
38
38
|
Brakeman.debug "Parsing #{path}"
|
39
|
-
|
39
|
+
RubyParser.new.parse input, path, @timeout
|
40
40
|
rescue Racc::ParseError => e
|
41
|
-
|
42
|
-
|
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
|
data/lib/brakeman/file_path.rb
CHANGED
@@ -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
|