spektr 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yaml +32 -0
- data/.gitignore +9 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +134 -0
- data/Guardfile +45 -0
- data/LICENSE.txt +27 -0
- data/README.md +70 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/spektr +7 -0
- data/lib/spektr/app.rb +209 -0
- data/lib/spektr/checks/base.rb +151 -0
- data/lib/spektr/checks/basic_auth.rb +27 -0
- data/lib/spektr/checks/basic_auth_timing.rb +24 -0
- data/lib/spektr/checks/command_injection.rb +48 -0
- data/lib/spektr/checks/content_tag_xss.rb +54 -0
- data/lib/spektr/checks/cookie_serialization.rb +21 -0
- data/lib/spektr/checks/create_with.rb +27 -0
- data/lib/spektr/checks/csrf.rb +25 -0
- data/lib/spektr/checks/csrf_setting.rb +39 -0
- data/lib/spektr/checks/default_routes.rb +43 -0
- data/lib/spektr/checks/deserialize.rb +62 -0
- data/lib/spektr/checks/detailed_exceptions.rb +29 -0
- data/lib/spektr/checks/digest_dos.rb +28 -0
- data/lib/spektr/checks/dynamic_finders.rb +26 -0
- data/lib/spektr/checks/evaluation.rb +25 -0
- data/lib/spektr/checks/file_access.rb +38 -0
- data/lib/spektr/checks/file_disclosure.rb +25 -0
- data/lib/spektr/checks/filter_skipping.rb +29 -0
- data/lib/spektr/checks/header_dos.rb +20 -0
- data/lib/spektr/checks/i18n_xss.rb +20 -0
- data/lib/spektr/checks/json_encoding.rb +23 -0
- data/lib/spektr/checks/json_entity_escape.rb +30 -0
- data/lib/spektr/checks/json_parsing.rb +47 -0
- data/lib/spektr/checks/link_to_href.rb +35 -0
- data/lib/spektr/checks/mass_assignment.rb +42 -0
- data/lib/spektr/checks/send.rb +24 -0
- data/lib/spektr/checks/sqli.rb +52 -0
- data/lib/spektr/checks/xss.rb +49 -0
- data/lib/spektr/checks.rb +9 -0
- data/lib/spektr/cli.rb +53 -0
- data/lib/spektr/erubi.rb +78 -0
- data/lib/spektr/exp/assignment.rb +20 -0
- data/lib/spektr/exp/base.rb +32 -0
- data/lib/spektr/exp/const.rb +7 -0
- data/lib/spektr/exp/definition.rb +32 -0
- data/lib/spektr/exp/ivasign.rb +7 -0
- data/lib/spektr/exp/lvasign.rb +7 -0
- data/lib/spektr/exp/send.rb +135 -0
- data/lib/spektr/exp/xstr.rb +12 -0
- data/lib/spektr/processors/base.rb +80 -0
- data/lib/spektr/processors/class_processor.rb +25 -0
- data/lib/spektr/targets/base.rb +119 -0
- data/lib/spektr/targets/config.rb +6 -0
- data/lib/spektr/targets/controller.rb +74 -0
- data/lib/spektr/targets/model.rb +6 -0
- data/lib/spektr/targets/routes.rb +38 -0
- data/lib/spektr/targets/view.rb +34 -0
- data/lib/spektr/version.rb +3 -0
- data/lib/spektr/warning.rb +23 -0
- data/lib/spektr.rb +120 -0
- data/spektr.gemspec +49 -0
- metadata +362 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks::Base
|
3
|
+
attr_accessor :name
|
4
|
+
|
5
|
+
def initialize(app, target)
|
6
|
+
@app = app
|
7
|
+
@target = target
|
8
|
+
@targets = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
::Spektr.logger.debug "Running #{self.class.name} on #{@target.path}"
|
13
|
+
target_affected? && should_run?
|
14
|
+
end
|
15
|
+
|
16
|
+
def target_affected?
|
17
|
+
@targets.include?(@target.class.name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def should_run?
|
21
|
+
if version_affected && @app.rails_version
|
22
|
+
version_affected > @app.rails_version
|
23
|
+
else
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def warn!(target, check, location, message, confidence = :high)
|
29
|
+
full_path = target.is_a?(String) ? target : target.path
|
30
|
+
path = full_path.gsub(@app.root, "")
|
31
|
+
return if dupe?(path, location, message)
|
32
|
+
|
33
|
+
@app.warnings << Warning.new(path, full_path, check, location, message, confidence)
|
34
|
+
end
|
35
|
+
|
36
|
+
def dupe?(path, location, message)
|
37
|
+
@app.warnings.find do |w|
|
38
|
+
w.path == path &&
|
39
|
+
(w.location.nil? || w.location&.line == location&.line) &&
|
40
|
+
w.message == message
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def version_affected; end
|
45
|
+
|
46
|
+
def user_input?(type, name, ast = nil, object = nil)
|
47
|
+
case type
|
48
|
+
when :ivar, :lvar
|
49
|
+
# TODO: handle helpers here too
|
50
|
+
return false unless @target.instance_of?(Spektr::Targets::View)
|
51
|
+
|
52
|
+
actions = []
|
53
|
+
@app.controllers.each do |controller|
|
54
|
+
actions = actions.concat controller.actions.select { |action|
|
55
|
+
action.template == @target.view_path
|
56
|
+
}
|
57
|
+
end
|
58
|
+
actions.each do |action|
|
59
|
+
action.body.each do |exp|
|
60
|
+
return exp.user_input? if exp.is_a?(Exp::Ivasign) && exp.name == name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
false
|
64
|
+
when :send
|
65
|
+
if ast.children.first&.type == :send
|
66
|
+
child = ast.children.first
|
67
|
+
return user_input?(child.type, child.children.last, child)
|
68
|
+
end
|
69
|
+
return true if %i[params cookies request].include? name
|
70
|
+
when :xstr, :begin
|
71
|
+
ast.children.each do |child|
|
72
|
+
next unless child.is_a?(Parser::AST::Node)
|
73
|
+
return true if user_input?(child.type, child.children.last, child)
|
74
|
+
end
|
75
|
+
when :dstr
|
76
|
+
object&.children&.each do |child|
|
77
|
+
if child.is_a?(Parser::AST::Node)
|
78
|
+
name = nil
|
79
|
+
ast = child
|
80
|
+
else
|
81
|
+
name = child.name
|
82
|
+
ast = child.ast
|
83
|
+
end
|
84
|
+
return true if user_input?(child.type, name, ast)
|
85
|
+
end
|
86
|
+
when :lvasgn
|
87
|
+
ast.children.each do |child|
|
88
|
+
next unless child.is_a?(Parser::AST::Node)
|
89
|
+
return true if user_input?(child.type, child.children.last, child)
|
90
|
+
end
|
91
|
+
when :block, :pair, :hash, :if
|
92
|
+
ast.children.each do |child|
|
93
|
+
next unless child.is_a?(Parser::AST::Node)
|
94
|
+
return true if user_input?(child.type, child.children.last, child)
|
95
|
+
end
|
96
|
+
when :sym, :str, :const, :int, :cbase, :true, :self, :args, :nil, :yield
|
97
|
+
# do nothing
|
98
|
+
else
|
99
|
+
raise "Unknown argument type #{type} #{name} #{ast.inspect}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# TODO: this doesn't work properly
|
104
|
+
def model_attribute?(item)
|
105
|
+
model_names = @app.models.collect(&:name)
|
106
|
+
case item.type
|
107
|
+
when :ivar, :lvar
|
108
|
+
# TODO: handle helpers here too
|
109
|
+
if ["Spektr::Targets::Controller", "Spektr::Targets::View"].include?(@target.class.name)
|
110
|
+
actions = []
|
111
|
+
@app.controllers.each do |controller|
|
112
|
+
actions = actions.concat controller.actions.select { |action|
|
113
|
+
action.template == @target.view_path if @target.respond_to? :view_path
|
114
|
+
}
|
115
|
+
end
|
116
|
+
actions.each do |action|
|
117
|
+
action.body.each do |exp|
|
118
|
+
return exp.user_input? if exp.is_a?(Exp::Ivasign) && exp.name == item.name
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
when :send
|
123
|
+
ast = item.is_a?(Parser::AST::Node) ? item : item.ast
|
124
|
+
_send = Exp::Send.new(ast)
|
125
|
+
return true if _send.receiver && model_names.include?(_send.receiver.name)
|
126
|
+
when :const
|
127
|
+
return true if model_names.include? item.name
|
128
|
+
when :block, :pair, :hash, :if
|
129
|
+
item.children.each do |child|
|
130
|
+
next unless child.is_a?(Parser::AST::Node)
|
131
|
+
return true if model_attribute?(child)
|
132
|
+
end
|
133
|
+
when :dstr
|
134
|
+
# TODO: implement this
|
135
|
+
when :sym, :str, :nil, :yield
|
136
|
+
# do nothing
|
137
|
+
else
|
138
|
+
raise "Unknown argument type #{item.type}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def app_version_between?(a, b)
|
143
|
+
version_between?(a, b, @app.rails_version)
|
144
|
+
end
|
145
|
+
|
146
|
+
def version_between?(a, b, version)
|
147
|
+
version = Gem::Version.new(version) unless version.is_a? Gem::Version
|
148
|
+
version >= Gem::Version.new(a) && version <= Gem::Version.new(b)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class BasicAuth < Base
|
4
|
+
|
5
|
+
def initialize(app, target)
|
6
|
+
super
|
7
|
+
@name = "Basic Authentication"
|
8
|
+
@type = "Password Plaintext Storage"
|
9
|
+
@targets = ["Spektr::Targets::Controller"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
return unless super
|
14
|
+
check_filter
|
15
|
+
end
|
16
|
+
|
17
|
+
def check_filter
|
18
|
+
calls = @target.find_calls(:http_basic_authenticate_with)
|
19
|
+
calls.each do |call|
|
20
|
+
if call.options[:password] && call.options[:password].value_type == :str
|
21
|
+
warn! @target, self, call.location, "Basic authentication password stored in source code"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class BasicAuthTiming < Base
|
4
|
+
|
5
|
+
def initialize(app, target)
|
6
|
+
super
|
7
|
+
@name = "Timing attack in basic auth (CVE-2015-7576)"
|
8
|
+
@type = "Timing attack"
|
9
|
+
@targets = ["Spektr::Targets::Controller"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
return unless super
|
14
|
+
if @target.find_calls(:http_basic_authenticate_with).any?
|
15
|
+
warn! @target, self, @target.find_calls(:http_basic_authenticate_with).first.location, "Basic authentication in Rails #{@app.rails_version} is vulnerable to timing attacks."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def version_affected
|
20
|
+
Gem::Version.new("4.2.5")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class CommandInjection < Base
|
4
|
+
def initialize(app, target)
|
5
|
+
super
|
6
|
+
@name = "Command Injection"
|
7
|
+
@type = "Command Injection"
|
8
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::Controller", "Spektr::Targets::Model", "Spektr::Targets::Routes", "Spektr::Targets::View"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
return unless super
|
13
|
+
# backticks
|
14
|
+
@target.find_xstr.each do |call|
|
15
|
+
argument = call.arguments.first
|
16
|
+
next unless argument
|
17
|
+
if user_input?(argument.type, argument.name, argument.ast, argument)
|
18
|
+
warn! @target, self, call.location, "Command injection in #{call.name}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
targets = ["IO", "Open3", "Kernel", "POSIX::Spawn", "Process", false]
|
23
|
+
methods = [:capture2, :capture2e, :capture3, :exec, :pipeline, :pipeline_r,
|
24
|
+
:pipeline_rw, :pipeline_start, :pipeline_w, :popen, :popen2, :popen2e,
|
25
|
+
:popen3, :spawn, :syscall, :system, :open]
|
26
|
+
targets.each do |target|
|
27
|
+
methods.each do |method|
|
28
|
+
check_calls(@target.find_calls(method, target))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_calls(calls)
|
34
|
+
# TODO: might need to exclude tempfile and ActiveStorage::Filename
|
35
|
+
calls.each do |call|
|
36
|
+
file_name = call.arguments.first
|
37
|
+
next unless file_name
|
38
|
+
if user_input?(file_name.type, file_name.name, file_name.ast, file_name)
|
39
|
+
warn! @target, self, call.location, "Command injection in #{call.name}"
|
40
|
+
# TODO: interpolation, but might be safe, we should make this better
|
41
|
+
elsif file_name.type == :dstr
|
42
|
+
warn! @target, self, call.location, "Command injection in #{call.name}", :low
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class ContentTagXss < Base
|
4
|
+
# Checks for unescaped values in `content_tag`
|
5
|
+
#
|
6
|
+
# content_tag :tag, body
|
7
|
+
# ^-- Unescaped in Rails 2.x
|
8
|
+
#
|
9
|
+
# content_tag, :tag, body, attribute => value
|
10
|
+
# ^-- Unescaped in all versions
|
11
|
+
# TODO:
|
12
|
+
# content_tag, :tag, body, attribute => value
|
13
|
+
# ^
|
14
|
+
# |
|
15
|
+
# Escaped by default, can be explicitly escaped
|
16
|
+
# or not by passing in (true|false) as fourth argument
|
17
|
+
def initialize(app, target)
|
18
|
+
super
|
19
|
+
@name = "XSS in content_tag"
|
20
|
+
@type = "Cross-Site Scripting"
|
21
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::View"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
return unless super
|
26
|
+
calls = @target.find_calls(:content_tag)
|
27
|
+
# https://groups.google.com/d/msg/ruby-security-ann/8B2iV2tPRSE/JkjCJkSoCgAJ
|
28
|
+
cve_2016_6316_check(calls)
|
29
|
+
|
30
|
+
calls.each do |call|
|
31
|
+
call.arguments.each do |argument|
|
32
|
+
if user_input?(argument.type, argument.name, argument.ast) && @app.rails_version < Gem::Version.new("3.0")
|
33
|
+
warn! @target, self, call.location, "Unescaped parameter in content_tag"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if call.options.any?
|
38
|
+
call.options.each_value do |option|
|
39
|
+
if user_input?(option.key.type, option.key.children.last)
|
40
|
+
warn! @target, self, call.location, "Unescaped attribute name in content_tag"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def cve_2016_6316_check(calls)
|
48
|
+
if calls.any? && app_version_between?("3.0.0", "3.2.22.3") || app_version_between?("4.0.0", "4.2.7.0") || app_version_between?("5.0.0", "5.0.0.0")
|
49
|
+
warn! @target, self, calls.first.location, "Rails #{@app.rails_version} does not escape double quotes in attribute values"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class CookieSerialization < Base
|
4
|
+
|
5
|
+
def initialize(app, target)
|
6
|
+
super
|
7
|
+
@name = "Unsafe deserialisation"
|
8
|
+
@type = "Insecure Deserialization"
|
9
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::Controller"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
return unless super
|
14
|
+
calls = @target.find_calls(:cookies_serializer=)
|
15
|
+
if calls.any?{ |call| call.receiver.expanded == "Rails.application.config.action_dispatch" && call.arguments.first.name == :marshal }
|
16
|
+
warn! @target, self, calls.first.location, "Marshal cookie serialization strategy can lead to remote code execution"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class CreateWith < Base
|
4
|
+
def initialize(app, target)
|
5
|
+
super
|
6
|
+
@name = "Strong parameter bypass (CVE-2014-3514)"
|
7
|
+
@type = "Input validation"
|
8
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::Controller", "Spektr::Targets::Routes", "Spektr::Targets::View"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
return unless super
|
13
|
+
if app_version_between?("4.0.0", "4.0.8") || app_version_between?("4.1.0", "4.1.5")
|
14
|
+
calls = @target.find_calls(:create_with)
|
15
|
+
calls.each do |call|
|
16
|
+
call.arguments.each do |argument|
|
17
|
+
if user_input?(argument.type, argument.name, argument.ast)
|
18
|
+
next if argument.ast.children[1] == :permit
|
19
|
+
warn! @target, self, call.location, "create_with is vulnerable to strong params bypass"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class Csrf < Base
|
4
|
+
def initialize(app, target)
|
5
|
+
super
|
6
|
+
@name = "CSRF token forgery vulnerability (CVE-2020-8166)"
|
7
|
+
@type = "Cross-Site Request Forgery"
|
8
|
+
@targets = ["Spektr::Targets::Base"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
# disable this
|
13
|
+
return false
|
14
|
+
return unless super
|
15
|
+
cve_2020_8186_check
|
16
|
+
end
|
17
|
+
|
18
|
+
def cve_2020_8186_check
|
19
|
+
if app_version_between?('0.0.0', '5.2.4.2') || app_version_between?('6.0.0', '6.0.3')
|
20
|
+
warn! @target, self, nil, "Rails #{@app.rails_version} has a vulnerability that may allow CSRF token forgery"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class CsrfSetting < Base
|
4
|
+
def initialize(app, target)
|
5
|
+
super
|
6
|
+
@name = 'Cross-Site Request Forgery'
|
7
|
+
@type = 'Cross-Site Request Forgery'
|
8
|
+
@targets = ['Spektr::Targets::Controller']
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
return unless super
|
13
|
+
return if @target.concern?
|
14
|
+
|
15
|
+
enabled = false
|
16
|
+
target = @target
|
17
|
+
while target
|
18
|
+
parent_controller = target.find_parent(@app.controllers)
|
19
|
+
enabled = parent_controller && parent_controller.find_calls(:protect_from_forgery).any?
|
20
|
+
break if enabled || parent_controller.nil?
|
21
|
+
|
22
|
+
target = parent_controller
|
23
|
+
end
|
24
|
+
return if enabled && @target.find_calls(:skip_forgery_protection).none?
|
25
|
+
|
26
|
+
if @target.find_calls(:protect_from_forgery).none? || (enabled && @target.find_calls(:skip_forgery_protection).any?)
|
27
|
+
skip = @target.find_calls(:skip_forgery_protection).last
|
28
|
+
return if enabled && skip && skip.options.keys.intersection(%i[only except]).any?
|
29
|
+
|
30
|
+
warn! @target, self, nil, 'protect_from_forgery should be enabled'
|
31
|
+
end
|
32
|
+
if @target.find_calls(:skip_forgery_protection).any?
|
33
|
+
return @target.find_calls(:skip_forgery_protection).last.options.keys.intersection(%i[only except]).any?
|
34
|
+
warn! @target, self, nil, 'protect_from_forgery should be enabled'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class DefaultRoutes < Base
|
4
|
+
def initialize(app, target)
|
5
|
+
super
|
6
|
+
@name = "Dangerous default routes"
|
7
|
+
@targets = ["Spektr::Targets::Routes"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
return unless super
|
12
|
+
@type = "Remote Code Execution"
|
13
|
+
check_for_cve_2014_0130
|
14
|
+
@type = "Default routes"
|
15
|
+
check_for_default_routes
|
16
|
+
end
|
17
|
+
|
18
|
+
def check_for_default_routes
|
19
|
+
if app_version_between?(3, 4)
|
20
|
+
calls = %w{ match get post put delete }.inject([]) do |memo, method|
|
21
|
+
memo.concat @target.find_calls(method.to_sym)
|
22
|
+
memo
|
23
|
+
end
|
24
|
+
calls.each do |call|
|
25
|
+
if call.arguments.first.name == ":controller(/:action(/:id(.:format)))" or (call.arguments.first.name.include?(":controller") && (call.arguments.first.name.include?(":action") or call.arguments.first.name.include?("*action")) )
|
26
|
+
warn! @target, self, call.location, "All public methods in controllers are available as actions"
|
27
|
+
end
|
28
|
+
|
29
|
+
if call.arguments.first.name.include?(":action") or call.arguments.first.name.include?("*action")
|
30
|
+
warn! @target, self, call.location, "All public methods in controllers are available as actions"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def check_for_cve_2014_0130
|
37
|
+
if app_version_between?("2.0.0", "2.3.18") || app_version_between?("3.0.0", "3.2.17") || app_version_between?("4.0.0", "4.0.4") || app_version_between?("4.1.0", "4.1.0")
|
38
|
+
warn! @target, self, nil, "#{@app.rails_version} with globbing routes is vulnerable to directory traversal and remote code execution."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class Deserialize < Base
|
4
|
+
|
5
|
+
def initialize(app, target)
|
6
|
+
super
|
7
|
+
@name = "Unsafe object deserialization"
|
8
|
+
@type = "Insecure Deserialization"
|
9
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::Controller", "Spektr::Targets::Routes", "Spektr::Targets::View"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
return unless super
|
14
|
+
check_csv
|
15
|
+
check_yaml
|
16
|
+
check_marshal
|
17
|
+
check_oj
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_csv
|
21
|
+
check_method(:load, "CSV")
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: handle safe yaml
|
25
|
+
def check_yaml
|
26
|
+
[:load_documents, :load_stream, :parse_documents, :parse_stream].each do |method|
|
27
|
+
check_method(method, "YAML")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_marshal
|
32
|
+
[:load, :restore].each do |method|
|
33
|
+
check_method(method, "Marshal")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def check_oj
|
38
|
+
check_method(:object_load, "Oj")
|
39
|
+
safe_default = false
|
40
|
+
safe_default = true if @target.find_calls(:mimic_JSON, "Oj").any?
|
41
|
+
call = @target.find_calls(:default_options=, "Oj").last
|
42
|
+
safe_default = true if call && call.options[:mode]&.value != :object
|
43
|
+
unless safe_default
|
44
|
+
check_method(:load, "Oj")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def check_method(method, receiver)
|
49
|
+
calls = @target.find_calls(method, receiver)
|
50
|
+
calls.each do |call|
|
51
|
+
argument = call.arguments.first
|
52
|
+
if argument.ast.type == :send && argument.ast.children.last.children.first.is_a?(Parser::AST::Node)
|
53
|
+
argument = Exp::Argument.new(argument.ast.children.last.children.first)
|
54
|
+
end
|
55
|
+
if user_input?(argument.type, argument.name, argument.ast)
|
56
|
+
warn! @target, self, call.location, "#{receiver}.#{method} is called with user supplied value"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class DetailedExceptions < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(app, target)
|
10
|
+
super
|
11
|
+
@name = "Information Disclosure"
|
12
|
+
@type = "Information Disclosure"
|
13
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::Controller"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
return unless super
|
18
|
+
call = @target.find_calls(:consider_all_requests_local=).last
|
19
|
+
if call && call.arguments.first.type == :true
|
20
|
+
warn! @target, self, call.location, "Detailed exceptions are enabled in production"
|
21
|
+
end
|
22
|
+
# TODO: make this better, by verifying that the method body is not empty, etc
|
23
|
+
if method = @target.find_method(:show_detailed_exceptions?)
|
24
|
+
warn! @target, self, method.location, "Detailed exceptions may be enabled in #{@target.name}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class DigestDos < Base
|
4
|
+
def initialize(app, target)
|
5
|
+
super
|
6
|
+
@name = "DoS in digest authentication(CVE-2012-3424)"
|
7
|
+
@type = "Denial of Service"
|
8
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::Controller"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
return unless super
|
13
|
+
return unless should_run?
|
14
|
+
calls = @target.find_calls(:authenticate_or_request_with_http_digest)
|
15
|
+
calls.concat(@target.find_calls(:authenticate_with_http_digest))
|
16
|
+
if calls.any?
|
17
|
+
warn! @target, self, calls.first.location, "Vulnerability in digest authentication CVE-2012-3424"
|
18
|
+
else
|
19
|
+
warn! "root", self, nil, "Vulnerability in digest authentication CVE-2012-3424"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_run?
|
24
|
+
app_version_between?("3.0.0", "3.0.15") || app_version_between?("3.1.0", "3.1.6") || app_version_between?("3.2.0", "3.2.5")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class DynamicFinders < Base
|
4
|
+
|
5
|
+
def initialize(app, target)
|
6
|
+
super
|
7
|
+
@name = "SQL Injection by unsafe usage of find_by_*"
|
8
|
+
@type = "SQL Injection"
|
9
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::Controller", "Spektr::Targets::View"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
return unless super
|
14
|
+
if app_version_between?("2.0.0", "4.1.99") && @app.has_gem?("mysql")
|
15
|
+
@target.find_calls(/^find_by_/).each do |call|
|
16
|
+
call.arguments.each do |argument|
|
17
|
+
if user_input?(argument.type, argument.name, argument.ast)
|
18
|
+
warn! @target, self, call.location, "MySQL integer conversion may cause 0 to match any string"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class Evaluation < Base
|
4
|
+
def initialize(app, target)
|
5
|
+
super
|
6
|
+
@name = "Arbitrary code execution"
|
7
|
+
@type = "Remote Code Execution"
|
8
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::Model", "Spektr::Targets::Controller", "Spektr::Targets::Routes", "Spektr::Targets::View"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
return unless super
|
13
|
+
[:eval, :instance_eval, :class_eval, :module_eval].each do |name|
|
14
|
+
@target.find_calls(name).each do |call|
|
15
|
+
call.arguments.each do |argument|
|
16
|
+
if user_input?(argument.type, argument.name, argument.ast)
|
17
|
+
warn! @target, self, call.location, "User input in eval"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Spektr
|
2
|
+
class Checks
|
3
|
+
class FileAccess < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
"File access"
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(app, target)
|
10
|
+
super
|
11
|
+
@name = "File access"
|
12
|
+
@type = "Information Disclosure"
|
13
|
+
@targets = ["Spektr::Targets::Base", "Spektr::Targets::Controller", "Spektr::Targets::Routes", "Spektr::Targets::View"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
return unless super
|
18
|
+
targets = ["Dir", "File", "IO", "Kernel", "Net::FTP", "Net::HTTP", "PStore", "Pathname", "Shell"]
|
19
|
+
methods = [:[], :chdir, :chroot, :delete, :entries, :foreach, :glob, :install, :lchmod, :lchown, :link, :load, :load_file, :makedirs, :move, :new, :open, :read, :readlines, :rename, :rmdir, :safe_unlink, :symlink, :syscopy, :sysopen, :truncate, :unlink]
|
20
|
+
targets.each do |target|
|
21
|
+
methods.each do |method|
|
22
|
+
check_calls_for_user_input(@target.find_calls(method, target))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def check_calls_for_user_input(calls)
|
28
|
+
calls.each do |call|
|
29
|
+
call.arguments.each do |argument|
|
30
|
+
if user_input?(argument.type, argument.name, argument.ast)
|
31
|
+
warn! @target, self, call.location, "#{argument.name} is used for a filename, which enables an attacker to access arbitrary files."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|