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