brakeman-lib 5.3.1 → 5.4.1
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 +21 -0
- data/README.md +2 -2
- data/lib/brakeman/checks/base_check.rb +2 -3
- data/lib/brakeman/checks/check_pathname.rb +48 -0
- data/lib/brakeman/checks/check_redirect.rb +65 -30
- data/lib/brakeman/checks/check_unscoped_find.rb +8 -0
- data/lib/brakeman/checks/check_weak_rsa_key.rb +112 -0
- data/lib/brakeman/checks/eol_check.rb +2 -2
- data/lib/brakeman/processors/alias_processor.rb +76 -22
- data/lib/brakeman/processors/gem_processor.rb +2 -2
- data/lib/brakeman/processors/lib/rails3_config_processor.rb +1 -1
- data/lib/brakeman/report/report_codeclimate.rb +1 -1
- data/lib/brakeman/rescanner.rb +3 -1
- data/lib/brakeman/scanner.rb +1 -1
- data/lib/brakeman/tracker/config.rb +68 -25
- data/lib/brakeman/tracker.rb +1 -1
- data/lib/brakeman/util.rb +20 -4
- data/lib/brakeman/version.rb +1 -1
- data/lib/brakeman/warning_codes.rb +4 -0
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1bd15d1d3f41a0fe1f537728a63f5fe432eae4d4b82cdb07233007e794b4b19a
|
4
|
+
data.tar.gz: 8fd1c274006689e0e391c5586c9bd59c7ce776a998c9313561185216e51f9519
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d3c99025931ba8a59c7852132cb80100f3f720de1ebfe632899e499a5784d5e92534efc6d9a04729bf3aec0d07c90fc2f69efccfd3d49558c277e370ae3d58f3
|
7
|
+
data.tar.gz: 571f89f8d5eb19b1d2c472517e3d66d819ab715379fd6b5bdd15cd136560499d8d86ff79da19a86a2c03830b8e0b0d0bf0dec3935267f75eacc95396a71d1f81
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
# 5.4.1 - 2023-02-21
|
2
|
+
|
3
|
+
* Fix file/line location for EOL software warnings
|
4
|
+
* Revise checking for request.env to only consider request headers
|
5
|
+
* Add `redirect_back` and `redirect_back_or_to` to open redirect check
|
6
|
+
* Support Rails 7 redirect options
|
7
|
+
* Add Rails 6.1 and 7.0 default configuration values
|
8
|
+
* Prevent redirects using `url_from` being marked as unsafe (Lachlan Sylvester)
|
9
|
+
* Warn about unscoped find for `find_by(id: ...)`
|
10
|
+
* Support `presence`, `presence_in` and `in?`
|
11
|
+
* Fix issue with `if` expressions in `when` clauses
|
12
|
+
|
13
|
+
# 5.4.0 - 2022-11-17
|
14
|
+
|
15
|
+
* Use relative paths for CodeClimate report format (Mike Poage)
|
16
|
+
* Add check for weak RSA key sizes and padding modes
|
17
|
+
* Handle multiple values and splats in case/when
|
18
|
+
* Ignore more model methods in redirects
|
19
|
+
* Add check for absolute paths issue with Pathname
|
20
|
+
* Fix `load_rails_defaults` overwriting settings in the Rails application (James Gregory-Monk)
|
21
|
+
|
1
22
|
# 5.3.1 - 2022-08-09
|
2
23
|
|
3
24
|
* Fix version range for CVE-2022-32209
|
data/README.md
CHANGED
@@ -64,9 +64,9 @@ Outside of Rails root (note that the output file is relative to path/to/rails/ap
|
|
64
64
|
|
65
65
|
# Compatibility
|
66
66
|
|
67
|
-
Brakeman should work with any version of Rails from 2.3.x to
|
67
|
+
Brakeman should work with any version of Rails from 2.3.x to 7.x.
|
68
68
|
|
69
|
-
Brakeman can analyze code written with Ruby 1.8 syntax and newer, but requires at least Ruby 2.
|
69
|
+
Brakeman can analyze code written with Ruby 1.8 syntax and newer, but requires at least Ruby 2.5.0 to run.
|
70
70
|
|
71
71
|
# Basic Options
|
72
72
|
|
@@ -76,7 +76,7 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
|
|
76
76
|
@has_user_input = Match.new(:params, exp)
|
77
77
|
elsif cookies? target
|
78
78
|
@has_user_input = Match.new(:cookies, exp)
|
79
|
-
elsif
|
79
|
+
elsif request_headers? target
|
80
80
|
@has_user_input = Match.new(:request, exp)
|
81
81
|
elsif sexp? target and model_name? target[1] #TODO: Can this be target.target?
|
82
82
|
@has_user_input = Match.new(:model, exp)
|
@@ -313,7 +313,7 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
|
|
313
313
|
return Match.new(:params, exp)
|
314
314
|
elsif cookies? exp
|
315
315
|
return Match.new(:cookies, exp)
|
316
|
-
elsif
|
316
|
+
elsif request_headers? exp
|
317
317
|
return Match.new(:request, exp)
|
318
318
|
else
|
319
319
|
has_immediate_user_input? exp.target
|
@@ -467,7 +467,6 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
|
|
467
467
|
version_between? version, "2.3.18.99", tracker.config.gem_version(:'railslts-version')
|
468
468
|
end
|
469
469
|
|
470
|
-
|
471
470
|
def version_between? low_version, high_version, current_version = nil
|
472
471
|
tracker.config.version_between? low_version, high_version, current_version
|
473
472
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'brakeman/checks/base_check'
|
2
|
+
|
3
|
+
class Brakeman::CheckPathname < Brakeman::BaseCheck
|
4
|
+
Brakeman::Checks.add self
|
5
|
+
|
6
|
+
@description = "Check for unexpected Pathname behavior"
|
7
|
+
|
8
|
+
def run_check
|
9
|
+
check_rails_root_join
|
10
|
+
check_pathname_join
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_rails_root_join
|
15
|
+
tracker.find_call(target: :'Rails.root', method: :join, nested: true).each do |result|
|
16
|
+
check_result result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_pathname_join
|
21
|
+
pathname_methods = [
|
22
|
+
:'Pathname.new',
|
23
|
+
:'Pathname.getwd',
|
24
|
+
:'Pathname.glob',
|
25
|
+
:'Pathname.pwd',
|
26
|
+
]
|
27
|
+
|
28
|
+
tracker.find_call(targets: pathname_methods, method: :join, nested: true).each do |result|
|
29
|
+
check_result result
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_result result
|
34
|
+
return unless original? result
|
35
|
+
|
36
|
+
result[:call].each_arg do |arg|
|
37
|
+
if match = has_immediate_user_input?(arg)
|
38
|
+
warn :result => result,
|
39
|
+
:warning_type => "Path Traversal",
|
40
|
+
:warning_code => :pathname_traversal,
|
41
|
+
:message => "Absolute paths in `Pathname#join` cause the entire path to be relative to the absolute path, ignoring any prior values",
|
42
|
+
:user_input => match,
|
43
|
+
:confidence => :high,
|
44
|
+
:cwe_id => [22]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -11,9 +11,7 @@ class Brakeman::CheckRedirect < Brakeman::BaseCheck
|
|
11
11
|
@description = "Looks for calls to redirect_to with user input as arguments"
|
12
12
|
|
13
13
|
def run_check
|
14
|
-
|
15
|
-
|
16
|
-
@model_find_calls = Set[:all, :create, :create!, :find, :find_by_sql, :first, :last, :new]
|
14
|
+
@model_find_calls = Set[:all, :create, :create!, :find, :find_by_sql, :first, :first!, :last, :last!, :new, :sole]
|
17
15
|
|
18
16
|
if tracker.options[:rails3]
|
19
17
|
@model_find_calls.merge [:from, :group, :having, :joins, :lock, :order, :reorder, :select, :where]
|
@@ -23,7 +21,13 @@ class Brakeman::CheckRedirect < Brakeman::BaseCheck
|
|
23
21
|
@model_find_calls.merge [:find_by, :find_by!, :take]
|
24
22
|
end
|
25
23
|
|
26
|
-
|
24
|
+
if version_between? "7.0.0", "9.9.9"
|
25
|
+
@model_find_calls << :find_sole_by
|
26
|
+
end
|
27
|
+
|
28
|
+
methods = [:redirect_to, :redirect_back, :redirect_back_or_to]
|
29
|
+
|
30
|
+
@tracker.find_call(:target => false, :methods => methods).each do |res|
|
27
31
|
process_result res
|
28
32
|
end
|
29
33
|
end
|
@@ -32,18 +36,28 @@ class Brakeman::CheckRedirect < Brakeman::BaseCheck
|
|
32
36
|
return unless original? result
|
33
37
|
|
34
38
|
call = result[:call]
|
35
|
-
method = call.method
|
36
|
-
|
37
39
|
opt = call.first_arg
|
38
40
|
|
39
|
-
|
41
|
+
# Location is specified with `fallback_location:`
|
42
|
+
# otherwise the arguments do not contain a location and
|
43
|
+
# the call can be ignored
|
44
|
+
if call.method == :redirect_back
|
45
|
+
if hash? opt and location = hash_access(opt, :fallback_location)
|
46
|
+
opt = location
|
47
|
+
else
|
48
|
+
return
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if not protected_by_raise?(call) and
|
40
53
|
not only_path?(call) and
|
41
54
|
not explicit_host?(opt) and
|
42
55
|
not slice_call?(opt) and
|
43
56
|
not safe_permit?(opt) and
|
44
|
-
|
57
|
+
not disallow_other_host?(call) and
|
58
|
+
res = include_user_input?(opt)
|
45
59
|
|
46
|
-
if res.type == :immediate
|
60
|
+
if res.type == :immediate and not allow_other_host?(call)
|
47
61
|
confidence = :high
|
48
62
|
else
|
49
63
|
confidence = :weak
|
@@ -64,42 +78,42 @@ class Brakeman::CheckRedirect < Brakeman::BaseCheck
|
|
64
78
|
#is being output directly. This is necessary because of tracker.options[:check_arguments]
|
65
79
|
#which can be used to enable/disable reporting output of method calls which use
|
66
80
|
#user input as arguments.
|
67
|
-
def include_user_input?
|
81
|
+
def include_user_input? opt, immediate = :immediate
|
68
82
|
Brakeman.debug "Checking if call includes user input"
|
69
83
|
|
70
|
-
arg = call.first_arg
|
71
|
-
|
72
84
|
# if the first argument is an array, rails assumes you are building a
|
73
85
|
# polymorphic route, which will never jump off-host
|
74
|
-
return false if array?
|
86
|
+
return false if array? opt
|
75
87
|
|
76
88
|
if tracker.options[:ignore_redirect_to_model]
|
77
|
-
if model_instance?(
|
89
|
+
if model_instance?(opt) or decorated_model?(opt)
|
78
90
|
return false
|
79
91
|
end
|
80
92
|
end
|
81
93
|
|
82
|
-
if res = has_immediate_model?(
|
83
|
-
unless call?
|
94
|
+
if res = has_immediate_model?(opt)
|
95
|
+
unless call? opt and opt.method.to_s =~ /_path/
|
84
96
|
return Match.new(immediate, res)
|
85
97
|
end
|
86
|
-
elsif call?
|
87
|
-
if request_value?
|
88
|
-
return Match.new(immediate,
|
89
|
-
elsif
|
90
|
-
return Match.new(immediate,
|
91
|
-
elsif arg.method == :url_for and include_user_input? arg
|
92
|
-
return Match.new(immediate, arg)
|
98
|
+
elsif call? opt
|
99
|
+
if request_value? opt
|
100
|
+
return Match.new(immediate, opt)
|
101
|
+
elsif opt.method == :url_for and include_user_input? opt.first_arg
|
102
|
+
return Match.new(immediate, opt)
|
93
103
|
#Ignore helpers like some_model_url?
|
94
|
-
elsif
|
104
|
+
elsif opt.method.to_s =~ /_(url|path)\z/
|
105
|
+
return false
|
106
|
+
elsif opt.method == :url_from
|
95
107
|
return false
|
96
108
|
end
|
97
|
-
elsif request_value?
|
98
|
-
return Match.new(immediate,
|
109
|
+
elsif request_value? opt
|
110
|
+
return Match.new(immediate, opt)
|
111
|
+
elsif node_type? opt, :or
|
112
|
+
return (include_user_input?(opt.lhs) or include_user_input?(opt.rhs))
|
99
113
|
end
|
100
114
|
|
101
|
-
if tracker.options[:check_arguments] and call?
|
102
|
-
include_user_input?
|
115
|
+
if tracker.options[:check_arguments] and call? opt
|
116
|
+
include_user_input? opt.first_arg, false #I'm doubting if this is really necessary...
|
103
117
|
else
|
104
118
|
false
|
105
119
|
end
|
@@ -204,7 +218,7 @@ class Brakeman::CheckRedirect < Brakeman::BaseCheck
|
|
204
218
|
def friendly_model? exp
|
205
219
|
call? exp and model_name? exp.target and exp.method == :friendly
|
206
220
|
end
|
207
|
-
|
221
|
+
|
208
222
|
#Returns true if exp is (probably) a decorated model instance
|
209
223
|
#using the Draper gem
|
210
224
|
def decorated_model? exp
|
@@ -245,7 +259,7 @@ class Brakeman::CheckRedirect < Brakeman::BaseCheck
|
|
245
259
|
if call? exp and params? exp.target and exp.method == :permit
|
246
260
|
exp.each_arg do |opt|
|
247
261
|
if symbol? opt and DANGEROUS_KEYS.include? opt.value
|
248
|
-
return false
|
262
|
+
return false
|
249
263
|
end
|
250
264
|
end
|
251
265
|
|
@@ -254,4 +268,25 @@ class Brakeman::CheckRedirect < Brakeman::BaseCheck
|
|
254
268
|
|
255
269
|
false
|
256
270
|
end
|
271
|
+
|
272
|
+
def protected_by_raise? call
|
273
|
+
raise_on_redirects? and
|
274
|
+
not allow_other_host? call
|
275
|
+
end
|
276
|
+
|
277
|
+
def raise_on_redirects?
|
278
|
+
@raise_on_redirects ||= true?(tracker.config.rails.dig(:action_controller, :raise_on_open_redirects))
|
279
|
+
end
|
280
|
+
|
281
|
+
def allow_other_host? call
|
282
|
+
opt = call.last_arg
|
283
|
+
|
284
|
+
hash? opt and true? hash_access(opt, :allow_other_host)
|
285
|
+
end
|
286
|
+
|
287
|
+
def disallow_other_host? call
|
288
|
+
opt = call.last_arg
|
289
|
+
|
290
|
+
hash? opt and false? hash_access(opt, :allow_other_host)
|
291
|
+
end
|
257
292
|
end
|
@@ -23,6 +23,14 @@ class Brakeman::CheckUnscopedFind < Brakeman::BaseCheck
|
|
23
23
|
calls.each do |call|
|
24
24
|
process_result call
|
25
25
|
end
|
26
|
+
|
27
|
+
tracker.find_call(:method => :find_by, :targets => associated_model_names).each do |result|
|
28
|
+
arg = result[:call].first_arg
|
29
|
+
|
30
|
+
if hash? arg and hash_access(arg, :id)
|
31
|
+
process_result result
|
32
|
+
end
|
33
|
+
end
|
26
34
|
end
|
27
35
|
|
28
36
|
def process_result result
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'brakeman/checks/base_check'
|
2
|
+
|
3
|
+
class Brakeman::CheckWeakRSAKey < Brakeman::BaseCheck
|
4
|
+
Brakeman::Checks.add self
|
5
|
+
|
6
|
+
@description = "Checks for weak uses RSA keys"
|
7
|
+
|
8
|
+
def run_check
|
9
|
+
check_rsa_key_creation
|
10
|
+
check_rsa_operations
|
11
|
+
end
|
12
|
+
|
13
|
+
def check_rsa_key_creation
|
14
|
+
tracker.find_call(targets: [:'OpenSSL::PKey::RSA'], method: [:new, :generate], nested: true).each do |result|
|
15
|
+
key_size_arg = result[:call].first_arg
|
16
|
+
check_key_size(result, key_size_arg)
|
17
|
+
end
|
18
|
+
|
19
|
+
tracker.find_call(targets: [:'OpenSSL::PKey'], method: [:generate_key], nested: true).each do |result|
|
20
|
+
call = result[:call]
|
21
|
+
key_type = call.first_arg
|
22
|
+
options_arg = call.second_arg
|
23
|
+
|
24
|
+
next unless options_arg and hash? options_arg
|
25
|
+
|
26
|
+
if string? key_type and key_type.value.upcase == 'RSA'
|
27
|
+
key_size_arg = (hash_access(options_arg, :rsa_keygen_bits) || hash_access(options_arg, s(:str, 'rsa_key_gen_bits')))
|
28
|
+
check_key_size(result, key_size_arg)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_rsa_operations
|
34
|
+
tracker.find_call(targets: [:'OpenSSL::PKey::RSA.new'], methods: [:public_encrypt, :public_decrypt, :private_encrypt, :private_decrypt], nested: true).each do |result|
|
35
|
+
padding_arg = result[:call].second_arg
|
36
|
+
check_padding(result, padding_arg)
|
37
|
+
end
|
38
|
+
|
39
|
+
tracker.find_call(targets: [:'OpenSSL::PKey.generate_key'], methods: [:encrypt, :decrypt, :sign, :verify, :sign_raw, :verify_raw], nested: true).each do |result|
|
40
|
+
call = result[:call]
|
41
|
+
options_arg = call.last_arg
|
42
|
+
|
43
|
+
if options_arg and hash? options_arg
|
44
|
+
padding_arg = (hash_access(options_arg, :rsa_padding_mode) || hash_access(options_arg, s(:str, 'rsa_padding_mode')))
|
45
|
+
else
|
46
|
+
padding_arg = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
check_padding(result, padding_arg)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_key_size result, key_size_arg
|
54
|
+
return unless number? key_size_arg
|
55
|
+
return unless original? result
|
56
|
+
|
57
|
+
key_size = key_size_arg.value
|
58
|
+
|
59
|
+
if key_size < 1024
|
60
|
+
confidence = :high
|
61
|
+
message = msg("RSA key with size ", msg_code(key_size.to_s), " is considered very weak. Use at least 2048 bit key size")
|
62
|
+
elsif key_size < 2048
|
63
|
+
confidence = :medium
|
64
|
+
message = msg("RSA key with size ", msg_code(key_size.to_s), " is considered weak. Use at least 2048 bit key size")
|
65
|
+
else
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
warn result: result,
|
70
|
+
warning_type: "Weak Cryptography",
|
71
|
+
warning_code: :small_rsa_key_size,
|
72
|
+
message: message,
|
73
|
+
confidence: confidence,
|
74
|
+
user_input: key_size_arg,
|
75
|
+
cwe_id: [326]
|
76
|
+
end
|
77
|
+
|
78
|
+
PKCS1_PADDING = s(:colon2, s(:colon2, s(:colon2, s(:const, :OpenSSL), :PKey), :RSA), :PKCS1_PADDING).freeze
|
79
|
+
PKCS1_PADDING_STR = s(:str, 'pkcs1').freeze
|
80
|
+
SSLV23_PADDING = s(:colon2, s(:colon2, s(:colon2, s(:const, :OpenSSL), :PKey), :RSA), :SSLV23_PADDING).freeze
|
81
|
+
SSLV23_PADDING_STR = s(:str, 'sslv23').freeze
|
82
|
+
NO_PADDING = s(:colon2, s(:colon2, s(:colon2, s(:const, :OpenSSL), :PKey), :RSA), :NO_PADDING).freeze
|
83
|
+
NO_PADDING_STR = s(:str, 'none').freeze
|
84
|
+
|
85
|
+
def check_padding result, padding_arg
|
86
|
+
return unless original? result
|
87
|
+
|
88
|
+
if string? padding_arg
|
89
|
+
padding_arg = padding_arg.deep_clone(padding_arg.line)
|
90
|
+
padding_arg.value.downcase!
|
91
|
+
end
|
92
|
+
|
93
|
+
case padding_arg
|
94
|
+
when PKCS1_PADDING, PKCS1_PADDING_STR, nil
|
95
|
+
message = "Use of padding mode PKCS1 (default if not specified), which is known to be insecure. Use OAEP instead"
|
96
|
+
when SSLV23_PADDING, SSLV23_PADDING_STR
|
97
|
+
message = "Use of padding mode SSLV23 for RSA key, which is only useful for outdated versions of SSL. Use OAEP instead"
|
98
|
+
when NO_PADDING, NO_PADDING_STR
|
99
|
+
message = "No padding mode used for RSA key. A safe padding mode (OAEP) should be specified for RSA keys"
|
100
|
+
else
|
101
|
+
return
|
102
|
+
end
|
103
|
+
|
104
|
+
warn result: result,
|
105
|
+
warning_type: "Weak Cryptography",
|
106
|
+
warning_code: :insecure_rsa_padding_mode,
|
107
|
+
message: message,
|
108
|
+
confidence: :high,
|
109
|
+
user_input: padding_arg,
|
110
|
+
cwe_id: [780]
|
111
|
+
end
|
112
|
+
end
|
@@ -34,7 +34,7 @@ class Brakeman::EOLCheck < Brakeman::BaseCheck
|
|
34
34
|
warning_code: :"pending_eol_#{library}",
|
35
35
|
message: msg("Support for ", msg_version(version, library.capitalize), " ends on #{eol_date}"),
|
36
36
|
confidence: confidence,
|
37
|
-
gem_info: gemfile_or_environment,
|
37
|
+
gem_info: gemfile_or_environment(library),
|
38
38
|
:cwe_id => [1104]
|
39
39
|
end
|
40
40
|
|
@@ -43,7 +43,7 @@ class Brakeman::EOLCheck < Brakeman::BaseCheck
|
|
43
43
|
warning_code: :"eol_#{library}",
|
44
44
|
message: msg("Support for ", msg_version(version, library.capitalize), " ended on #{eol_date}"),
|
45
45
|
confidence: :high,
|
46
|
-
gem_info: gemfile_or_environment,
|
46
|
+
gem_info: gemfile_or_environment(library),
|
47
47
|
:cwe_id => [1104]
|
48
48
|
end
|
49
49
|
end
|
@@ -300,11 +300,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
|
|
300
300
|
if array? target and first_arg.nil? and sexp? target[1]
|
301
301
|
exp = target[1]
|
302
302
|
end
|
303
|
-
when :freeze
|
304
|
-
unless target.nil?
|
305
|
-
exp = target
|
306
|
-
end
|
307
|
-
when :dup
|
303
|
+
when :freeze, :dup, :presence
|
308
304
|
unless target.nil?
|
309
305
|
exp = target
|
310
306
|
end
|
@@ -332,6 +328,17 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
|
|
332
328
|
exp = res
|
333
329
|
end
|
334
330
|
end
|
331
|
+
when :presence_in
|
332
|
+
arg = exp.first_arg
|
333
|
+
|
334
|
+
if node_type? arg, :array
|
335
|
+
# 1.presence_in [1,2,3]
|
336
|
+
if arg.include? target
|
337
|
+
exp = target
|
338
|
+
elsif all_literals? arg
|
339
|
+
exp = safe_literal(exp.line)
|
340
|
+
end
|
341
|
+
end
|
335
342
|
end
|
336
343
|
|
337
344
|
exp
|
@@ -862,6 +869,17 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
|
|
862
869
|
(all_literals? exp.target or dir_glob? exp.target)
|
863
870
|
end
|
864
871
|
|
872
|
+
# Check if exp is a call to Array#include? on an array literal
|
873
|
+
# that contains all literal values. For example:
|
874
|
+
#
|
875
|
+
# x.in? [1, 2, "a"]
|
876
|
+
#
|
877
|
+
def in_array_all_literals? exp
|
878
|
+
call? exp and
|
879
|
+
exp.method == :in? and
|
880
|
+
all_literals? exp.first_arg
|
881
|
+
end
|
882
|
+
|
865
883
|
# Check if exp is a call to Hash#include? on a hash literal
|
866
884
|
# that contains all literal values. For example:
|
867
885
|
#
|
@@ -915,28 +933,30 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
|
|
915
933
|
scope do
|
916
934
|
@branch_env = env.current
|
917
935
|
branch_index = 2 + i # s(:if, condition, then_branch, else_branch)
|
918
|
-
|
936
|
+
exp[branch_index] = if i == 0 and hash_or_array_include_all_literals? condition
|
919
937
|
# If the condition is ["a", "b"].include? x
|
920
|
-
# set x to
|
938
|
+
# set x to safe_literal inside the true branch
|
921
939
|
var = condition.first_arg
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
940
|
+
value = safe_literal(var.line)
|
941
|
+
process_branch_with_value(var, value, branch, i)
|
942
|
+
elsif i == 0 and in_array_all_literals? condition
|
943
|
+
# If the condition is x.in? ["a", "b"]
|
944
|
+
# set x to safe_literal inside the true branch
|
945
|
+
var = condition.target
|
946
|
+
value = safe_literal(var.line)
|
947
|
+
process_branch_with_value(var, value, branch, i)
|
926
948
|
elsif i == 0 and equality_check? condition
|
927
949
|
# For conditions like a == b,
|
928
950
|
# set a to b inside the true branch
|
929
951
|
var = condition.target
|
930
|
-
|
931
|
-
|
932
|
-
exp[branch_index] = process_if_branch branch
|
933
|
-
env.current[var] = previous_value
|
952
|
+
value = condition.first_arg
|
953
|
+
process_branch_with_value(var, value, branch, i)
|
934
954
|
elsif i == 1 and hash_or_array_include_all_literals? condition and early_return? branch
|
935
955
|
var = condition.first_arg
|
936
956
|
env.current[var] = safe_literal(var.line)
|
937
|
-
|
957
|
+
process_if_branch branch
|
938
958
|
else
|
939
|
-
|
959
|
+
process_if_branch branch
|
940
960
|
end
|
941
961
|
branch_scopes << env.current
|
942
962
|
@branch_env = nil
|
@@ -953,6 +973,14 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
|
|
953
973
|
exp
|
954
974
|
end
|
955
975
|
|
976
|
+
def process_branch_with_value var, value, branch, branch_index
|
977
|
+
previous_value = env.current[var]
|
978
|
+
env.current[var] = value
|
979
|
+
result = process_if_branch branch
|
980
|
+
env.current[var] = previous_value
|
981
|
+
result
|
982
|
+
end
|
983
|
+
|
956
984
|
def early_return? exp
|
957
985
|
return true if node_type? exp, :return
|
958
986
|
return true if call? exp and [:fail, :raise].include? exp.method
|
@@ -970,11 +998,27 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
|
|
970
998
|
exp.method == :==
|
971
999
|
end
|
972
1000
|
|
1001
|
+
# Not a list of values
|
1002
|
+
# when :example
|
973
1003
|
def simple_when? exp
|
974
1004
|
node_type? exp[1], :array and
|
975
|
-
|
976
|
-
|
977
|
-
|
1005
|
+
exp[1].length == 2 and # only one element in the array
|
1006
|
+
not node_type? exp[1][1], :splat, :array
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
# A list of literal values
|
1010
|
+
#
|
1011
|
+
# when 1,2,3
|
1012
|
+
#
|
1013
|
+
# or
|
1014
|
+
#
|
1015
|
+
# when *[:a, :b]
|
1016
|
+
def all_literals_when? exp
|
1017
|
+
if array? exp[1] # pretty sure this is always true
|
1018
|
+
all_literals? exp[1] or # simple list, not actually array
|
1019
|
+
(splat_array? exp[1][1] and
|
1020
|
+
all_literals? exp[1][1][1])
|
1021
|
+
end
|
978
1022
|
end
|
979
1023
|
|
980
1024
|
def process_case exp
|
@@ -1000,11 +1044,21 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
|
|
1000
1044
|
exp.each_sexp do |e|
|
1001
1045
|
if node_type? e, :when
|
1002
1046
|
scope do
|
1047
|
+
# Process the when value for matching
|
1048
|
+
process_default e[1]
|
1049
|
+
|
1050
|
+
# Moved here to avoid @branch_env being cleared out
|
1051
|
+
# in process_default
|
1052
|
+
# Maybe in the future don't set it to nil?
|
1003
1053
|
@branch_env = env.current
|
1004
1054
|
|
1005
1055
|
# set value of case var if possible
|
1006
|
-
if case_value
|
1007
|
-
|
1056
|
+
if case_value
|
1057
|
+
if simple_when? e
|
1058
|
+
@branch_env[case_value] = e[1][1]
|
1059
|
+
elsif all_literals_when? e
|
1060
|
+
@branch_env[case_value] = safe_literal(e.line + 1)
|
1061
|
+
end
|
1008
1062
|
end
|
1009
1063
|
|
1010
1064
|
# when blocks aren't blocks, they are lists of expressions
|
@@ -56,7 +56,7 @@ class Brakeman::GemProcessor < Brakeman::BasicProcessor
|
|
56
56
|
elsif exp.method == :ruby
|
57
57
|
version = exp.first_arg
|
58
58
|
if string? version
|
59
|
-
@tracker.config.set_ruby_version version.value
|
59
|
+
@tracker.config.set_ruby_version version.value, @gemfile, exp.line
|
60
60
|
end
|
61
61
|
end
|
62
62
|
elsif @inside_gemspec and exp.method == :add_dependency
|
@@ -97,7 +97,7 @@ class Brakeman::GemProcessor < Brakeman::BasicProcessor
|
|
97
97
|
if line =~ @gem_name_version
|
98
98
|
@tracker.config.add_gem $1, $2, file, line_num
|
99
99
|
elsif line =~ @ruby_version
|
100
|
-
@tracker.config.set_ruby_version $1
|
100
|
+
@tracker.config.set_ruby_version $1, file, line_num
|
101
101
|
end
|
102
102
|
end
|
103
103
|
end
|
@@ -86,7 +86,7 @@ class Brakeman::Rails3ConfigProcessor < Brakeman::BasicProcessor
|
|
86
86
|
end
|
87
87
|
elsif include_rails_config? exp
|
88
88
|
options_path = get_rails_config exp
|
89
|
-
@tracker.config.set_rails_config(exp.first_arg,
|
89
|
+
@tracker.config.set_rails_config(value: exp.first_arg, path: options_path, overwrite: true)
|
90
90
|
end
|
91
91
|
|
92
92
|
exp
|
data/lib/brakeman/rescanner.rb
CHANGED
@@ -6,7 +6,7 @@ require 'brakeman/differ'
|
|
6
6
|
class Brakeman::Rescanner < Brakeman::Scanner
|
7
7
|
include Brakeman::Util
|
8
8
|
KNOWN_TEMPLATE_EXTENSIONS = Brakeman::TemplateParser::KNOWN_TEMPLATE_EXTENSIONS
|
9
|
-
SCAN_ORDER = [:
|
9
|
+
SCAN_ORDER = [:gemfile, :config, :initializer, :lib, :routes, :template,
|
10
10
|
:model, :controller]
|
11
11
|
|
12
12
|
#Create new Rescanner to scan changed files
|
@@ -332,6 +332,8 @@ class Brakeman::Rescanner < Brakeman::Scanner
|
|
332
332
|
:routes
|
333
333
|
when /\/config\/.+\.(rb|yml)/
|
334
334
|
:config
|
335
|
+
when /\.ruby-version/
|
336
|
+
:config
|
335
337
|
when /Gemfile|gems\./
|
336
338
|
:gemfile
|
337
339
|
else
|
data/lib/brakeman/scanner.rb
CHANGED
@@ -138,7 +138,7 @@ class Brakeman::Scanner
|
|
138
138
|
|
139
139
|
if @app_tree.exists? ".ruby-version"
|
140
140
|
if version = @app_tree.file_path(".ruby-version").read[/(\d\.\d.\d+)/]
|
141
|
-
tracker.config.set_ruby_version version
|
141
|
+
tracker.config.set_ruby_version version, @app_tree.file_path(".ruby-version"), 1
|
142
142
|
end
|
143
143
|
end
|
144
144
|
|
@@ -129,8 +129,9 @@ module Brakeman
|
|
129
129
|
@rails_version
|
130
130
|
end
|
131
131
|
|
132
|
-
def set_ruby_version version
|
132
|
+
def set_ruby_version version, file, line
|
133
133
|
@ruby_version = extract_version(version)
|
134
|
+
add_gem :ruby, @ruby_version, file, line
|
134
135
|
end
|
135
136
|
|
136
137
|
def extract_version version
|
@@ -166,7 +167,7 @@ module Brakeman
|
|
166
167
|
# then this will set
|
167
168
|
#
|
168
169
|
# rails[:action_controller][:perform_caching] = value
|
169
|
-
def set_rails_config value
|
170
|
+
def set_rails_config value:, path:, overwrite: false
|
170
171
|
config = self.rails
|
171
172
|
|
172
173
|
path[0..-2].each do |o|
|
@@ -182,7 +183,9 @@ module Brakeman
|
|
182
183
|
config = option
|
183
184
|
end
|
184
185
|
|
185
|
-
config[path.last]
|
186
|
+
if overwrite || config[path.last].nil?
|
187
|
+
config[path.last] = value
|
188
|
+
end
|
186
189
|
end
|
187
190
|
|
188
191
|
# Load defaults based on config.load_defaults value
|
@@ -195,38 +198,78 @@ module Brakeman
|
|
195
198
|
false_value = Sexp.new(:false)
|
196
199
|
|
197
200
|
if version >= 5.0
|
198
|
-
set_rails_config(true_value, :action_controller, :per_form_csrf_tokens)
|
199
|
-
set_rails_config(true_value, :action_controller, :forgery_protection_origin_check)
|
200
|
-
set_rails_config(true_value, :active_record, :belongs_to_required_by_default)
|
201
|
+
set_rails_config(value: true_value, path: [:action_controller, :per_form_csrf_tokens])
|
202
|
+
set_rails_config(value: true_value, path: [:action_controller, :forgery_protection_origin_check])
|
203
|
+
set_rails_config(value: true_value, path: [:active_record, :belongs_to_required_by_default])
|
201
204
|
# Note: this may need to be changed, because ssl_options is a Hash
|
202
|
-
set_rails_config(true_value, :ssl_options, :hsts, :subdomains)
|
205
|
+
set_rails_config(value: true_value, path: [:ssl_options, :hsts, :subdomains])
|
203
206
|
end
|
204
207
|
|
205
208
|
if version >= 5.1
|
206
|
-
set_rails_config(false_value, :assets, :unknown_asset_fallback)
|
207
|
-
set_rails_config(true_value, :action_view, :form_with_generates_remote_forms)
|
209
|
+
set_rails_config(value: false_value, path: [:assets, :unknown_asset_fallback])
|
210
|
+
set_rails_config(value: true_value, path: [:action_view, :form_with_generates_remote_forms])
|
208
211
|
end
|
209
212
|
|
210
213
|
if version >= 5.2
|
211
|
-
set_rails_config(true_value, :active_record, :cache_versioning)
|
212
|
-
set_rails_config(true_value, :action_dispatch, :use_authenticated_cookie_encryption)
|
213
|
-
set_rails_config(true_value, :active_support, :use_authenticated_message_encryption)
|
214
|
-
set_rails_config(true_value, :active_support, :use_sha1_digests)
|
215
|
-
set_rails_config(true_value, :action_controller, :default_protect_from_forgery)
|
216
|
-
set_rails_config(true_value, :action_view, :form_with_generates_ids)
|
214
|
+
set_rails_config(value: true_value, path: [:active_record, :cache_versioning])
|
215
|
+
set_rails_config(value: true_value, path: [:action_dispatch, :use_authenticated_cookie_encryption])
|
216
|
+
set_rails_config(value: true_value, path: [:active_support, :use_authenticated_message_encryption])
|
217
|
+
set_rails_config(value: true_value, path: [:active_support, :use_sha1_digests])
|
218
|
+
set_rails_config(value: true_value, path: [:action_controller, :default_protect_from_forgery])
|
219
|
+
set_rails_config(value: true_value, path: [:action_view, :form_with_generates_ids])
|
217
220
|
end
|
218
221
|
|
219
222
|
if version >= 6.0
|
220
|
-
set_rails_config(Sexp.new(:lit, :zeitwerk), :autoloader)
|
221
|
-
set_rails_config(false_value, :action_view, :default_enforce_utf8)
|
222
|
-
set_rails_config(true_value, :action_dispatch, :use_cookies_with_metadata)
|
223
|
-
set_rails_config(false_value, :action_dispatch, :return_only_media_type_on_content_type)
|
224
|
-
set_rails_config(Sexp.new(:str, 'ActionMailer::MailDeliveryJob'), :action_mailer, :delivery_job)
|
225
|
-
set_rails_config(true_value, :active_job, :return_false_on_aborted_enqueue)
|
226
|
-
set_rails_config(Sexp.new(:lit, :active_storage_analysis), :active_storage, :queues, :analysis)
|
227
|
-
set_rails_config(Sexp.new(:lit, :active_storage_purge), :active_storage, :queues, :purge)
|
228
|
-
set_rails_config(true_value, :active_storage, :replace_on_assign_to_many)
|
229
|
-
set_rails_config(true_value, :active_record, :collection_cache_versioning)
|
223
|
+
set_rails_config(value: Sexp.new(:lit, :zeitwerk), path: [:autoloader])
|
224
|
+
set_rails_config(value: false_value, path: [:action_view, :default_enforce_utf8])
|
225
|
+
set_rails_config(value: true_value, path: [:action_dispatch, :use_cookies_with_metadata])
|
226
|
+
set_rails_config(value: false_value, path: [:action_dispatch, :return_only_media_type_on_content_type])
|
227
|
+
set_rails_config(value: Sexp.new(:str, 'ActionMailer::MailDeliveryJob'), path: [:action_mailer, :delivery_job])
|
228
|
+
set_rails_config(value: true_value, path: [:active_job, :return_false_on_aborted_enqueue])
|
229
|
+
set_rails_config(value: Sexp.new(:lit, :active_storage_analysis), path: [:active_storage, :queues, :analysis])
|
230
|
+
set_rails_config(value: Sexp.new(:lit, :active_storage_purge), path: [:active_storage, :queues, :purge])
|
231
|
+
set_rails_config(value: true_value, path: [:active_storage, :replace_on_assign_to_many])
|
232
|
+
set_rails_config(value: true_value, path: [:active_record, :collection_cache_versioning])
|
233
|
+
end
|
234
|
+
|
235
|
+
if version >= 6.1
|
236
|
+
set_rails_config(value: true_value, path: [:action_controller, :urlsafe_csrf_tokens])
|
237
|
+
set_rails_config(value: Sexp.new(:lit, :lax), path: [:action_dispatch, :cookies_same_site_protection])
|
238
|
+
set_rails_config(value: Sexp.new(:lit, 308), path: [:action_dispatch, :ssl_default_redirect_status])
|
239
|
+
set_rails_config(value: false_value, path: [:action_view, :form_with_generates_remote_forms])
|
240
|
+
set_rails_config(value: true_value, path: [:action_view, :preload_links_header])
|
241
|
+
set_rails_config(value: Sexp.new(:lit, 0.15), path: [:active_job, :retry_jitter])
|
242
|
+
set_rails_config(value: true_value, path: [:active_record, :has_many_inversing])
|
243
|
+
set_rails_config(value: false_value, path: [:active_record, :legacy_connection_handling])
|
244
|
+
set_rails_config(value: true_value, path: [:active_storage, :track_variants])
|
245
|
+
end
|
246
|
+
|
247
|
+
if version >= 7.0
|
248
|
+
video_args =
|
249
|
+
Sexp.new(:str, "-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2")
|
250
|
+
hash_class = s(:colon2, s(:colon2, s(:const, :OpenSSL), :Digest), :SHA256)
|
251
|
+
|
252
|
+
set_rails_config(value: true_value, path: [:action_controller, :raise_on_open_redirects])
|
253
|
+
set_rails_config(value: true_value, path: [:action_controller, :wrap_parameters_by_default])
|
254
|
+
set_rails_config(value: Sexp.new(:lit, :json), path: [:action_dispatch, :cookies_serializer])
|
255
|
+
set_rails_config(value: false_value, path: [:action_dispatch, :return_only_request_media_type_on_content_type])
|
256
|
+
set_rails_config(value: Sexp.new(:lit, 5), path: [:action_mailer, :smtp_timeout])
|
257
|
+
set_rails_config(value: false_value, path: [:action_view, :apply_stylesheet_media_default])
|
258
|
+
set_rails_config(value: true_value, path: [:ction_view, :button_to_generates_button_tag])
|
259
|
+
set_rails_config(value: true_value, path: [:active_record, :automatic_scope_inversing])
|
260
|
+
set_rails_config(value: false_value, path: [:active_record, :partial_inserts])
|
261
|
+
set_rails_config(value: true_value, path: [:active_record, :verify_foreign_keys_for_fixtures])
|
262
|
+
set_rails_config(value: true_value, path: [:active_storage, :multiple_file_field_include_hidden])
|
263
|
+
set_rails_config(value: Sexp.new(:lit, :vips), path: [:active_storage, :variant_processor])
|
264
|
+
set_rails_config(value: video_args, path: [:active_storage, :video_preview_arguments])
|
265
|
+
set_rails_config(value: Sexp.new(:lit, 7.0), path: [:active_support, :cache_format_version])
|
266
|
+
set_rails_config(value: true_value, path: [:active_support, :disable_to_s_conversion])
|
267
|
+
set_rails_config(value: true_value, path: [:active_support, :executor_around_test_case])
|
268
|
+
set_rails_config(value: hash_class, path: [:active_support, :hash_digest_class])
|
269
|
+
set_rails_config(value: Sexp.new(:lit, :thread), path: [:active_support, :isolation_level])
|
270
|
+
set_rails_config(value: hash_class, path: [:active_support, :key_generator_hash_digest_class])
|
271
|
+
set_rails_config(value: true_value, path: [:active_support, :remove_deprecated_time_with_zone_name])
|
272
|
+
set_rails_config(value: true_value, path: [:active_support, :use_rfc4122_namespaced_uuids])
|
230
273
|
end
|
231
274
|
end
|
232
275
|
end
|
data/lib/brakeman/tracker.rb
CHANGED
data/lib/brakeman/util.rb
CHANGED
@@ -265,15 +265,31 @@ module Brakeman::Util
|
|
265
265
|
false
|
266
266
|
end
|
267
267
|
|
268
|
-
|
269
|
-
|
268
|
+
# Only return true when accessing request headers via request.env[...]
|
269
|
+
def request_headers? exp
|
270
|
+
return unless sexp? exp
|
271
|
+
|
272
|
+
if exp[1] == REQUEST_ENV
|
273
|
+
if exp.method == :[]
|
274
|
+
if string? exp.first_arg
|
275
|
+
# Only care about HTTP headers, which are prefixed by 'HTTP_'
|
276
|
+
exp.first_arg.value.start_with?('HTTP_'.freeze)
|
277
|
+
else
|
278
|
+
true # request.env[something]
|
279
|
+
end
|
280
|
+
else
|
281
|
+
false # request.env.something
|
282
|
+
end
|
283
|
+
else
|
284
|
+
false
|
285
|
+
end
|
270
286
|
end
|
271
287
|
|
272
|
-
#Check if exp is params, cookies, or
|
288
|
+
#Check if exp is params, cookies, or request_headers
|
273
289
|
def request_value? exp
|
274
290
|
params? exp or
|
275
291
|
cookies? exp or
|
276
|
-
|
292
|
+
request_headers? exp
|
277
293
|
end
|
278
294
|
|
279
295
|
def constant? exp
|
data/lib/brakeman/version.rb
CHANGED
@@ -126,6 +126,10 @@ module Brakeman::WarningCodes
|
|
126
126
|
:pending_eol_rails => 122,
|
127
127
|
:pending_eol_ruby => 123,
|
128
128
|
:CVE_2022_32209 => 124,
|
129
|
+
:pathname_traversal => 125,
|
130
|
+
:insecure_rsa_padding_mode => 126,
|
131
|
+
:missing_rsa_padding_mode => 127,
|
132
|
+
:small_rsa_key_size => 128,
|
129
133
|
|
130
134
|
:custom_check => 9090,
|
131
135
|
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: brakeman-lib
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Collins
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -299,6 +299,7 @@ files:
|
|
299
299
|
- lib/brakeman/checks/check_nested_attributes_bypass.rb
|
300
300
|
- lib/brakeman/checks/check_number_to_currency.rb
|
301
301
|
- lib/brakeman/checks/check_page_caching_cve.rb
|
302
|
+
- lib/brakeman/checks/check_pathname.rb
|
302
303
|
- lib/brakeman/checks/check_permit_attributes.rb
|
303
304
|
- lib/brakeman/checks/check_quote_table_name.rb
|
304
305
|
- lib/brakeman/checks/check_redirect.rb
|
@@ -337,6 +338,7 @@ files:
|
|
337
338
|
- lib/brakeman/checks/check_validation_regex.rb
|
338
339
|
- lib/brakeman/checks/check_verb_confusion.rb
|
339
340
|
- lib/brakeman/checks/check_weak_hash.rb
|
341
|
+
- lib/brakeman/checks/check_weak_rsa_key.rb
|
340
342
|
- lib/brakeman/checks/check_without_protection.rb
|
341
343
|
- lib/brakeman/checks/check_xml_dos.rb
|
342
344
|
- lib/brakeman/checks/check_yaml_parsing.rb
|
@@ -448,7 +450,7 @@ metadata:
|
|
448
450
|
mailing_list_uri: https://gitter.im/presidentbeef/brakeman
|
449
451
|
source_code_uri: https://github.com/presidentbeef/brakeman
|
450
452
|
wiki_uri: https://github.com/presidentbeef/brakeman/wiki
|
451
|
-
post_install_message:
|
453
|
+
post_install_message:
|
452
454
|
rdoc_options: []
|
453
455
|
require_paths:
|
454
456
|
- lib
|
@@ -463,8 +465,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
463
465
|
- !ruby/object:Gem::Version
|
464
466
|
version: '0'
|
465
467
|
requirements: []
|
466
|
-
rubygems_version: 3.
|
467
|
-
signing_key:
|
468
|
+
rubygems_version: 3.3.3
|
469
|
+
signing_key:
|
468
470
|
specification_version: 4
|
469
471
|
summary: Security vulnerability scanner for Ruby on Rails.
|
470
472
|
test_files: []
|