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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yaml +32 -0
  3. data/.gitignore +9 -0
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +3 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.lock +134 -0
  9. data/Guardfile +45 -0
  10. data/LICENSE.txt +27 -0
  11. data/README.md +70 -0
  12. data/Rakefile +10 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/bin/spektr +7 -0
  16. data/lib/spektr/app.rb +209 -0
  17. data/lib/spektr/checks/base.rb +151 -0
  18. data/lib/spektr/checks/basic_auth.rb +27 -0
  19. data/lib/spektr/checks/basic_auth_timing.rb +24 -0
  20. data/lib/spektr/checks/command_injection.rb +48 -0
  21. data/lib/spektr/checks/content_tag_xss.rb +54 -0
  22. data/lib/spektr/checks/cookie_serialization.rb +21 -0
  23. data/lib/spektr/checks/create_with.rb +27 -0
  24. data/lib/spektr/checks/csrf.rb +25 -0
  25. data/lib/spektr/checks/csrf_setting.rb +39 -0
  26. data/lib/spektr/checks/default_routes.rb +43 -0
  27. data/lib/spektr/checks/deserialize.rb +62 -0
  28. data/lib/spektr/checks/detailed_exceptions.rb +29 -0
  29. data/lib/spektr/checks/digest_dos.rb +28 -0
  30. data/lib/spektr/checks/dynamic_finders.rb +26 -0
  31. data/lib/spektr/checks/evaluation.rb +25 -0
  32. data/lib/spektr/checks/file_access.rb +38 -0
  33. data/lib/spektr/checks/file_disclosure.rb +25 -0
  34. data/lib/spektr/checks/filter_skipping.rb +29 -0
  35. data/lib/spektr/checks/header_dos.rb +20 -0
  36. data/lib/spektr/checks/i18n_xss.rb +20 -0
  37. data/lib/spektr/checks/json_encoding.rb +23 -0
  38. data/lib/spektr/checks/json_entity_escape.rb +30 -0
  39. data/lib/spektr/checks/json_parsing.rb +47 -0
  40. data/lib/spektr/checks/link_to_href.rb +35 -0
  41. data/lib/spektr/checks/mass_assignment.rb +42 -0
  42. data/lib/spektr/checks/send.rb +24 -0
  43. data/lib/spektr/checks/sqli.rb +52 -0
  44. data/lib/spektr/checks/xss.rb +49 -0
  45. data/lib/spektr/checks.rb +9 -0
  46. data/lib/spektr/cli.rb +53 -0
  47. data/lib/spektr/erubi.rb +78 -0
  48. data/lib/spektr/exp/assignment.rb +20 -0
  49. data/lib/spektr/exp/base.rb +32 -0
  50. data/lib/spektr/exp/const.rb +7 -0
  51. data/lib/spektr/exp/definition.rb +32 -0
  52. data/lib/spektr/exp/ivasign.rb +7 -0
  53. data/lib/spektr/exp/lvasign.rb +7 -0
  54. data/lib/spektr/exp/send.rb +135 -0
  55. data/lib/spektr/exp/xstr.rb +12 -0
  56. data/lib/spektr/processors/base.rb +80 -0
  57. data/lib/spektr/processors/class_processor.rb +25 -0
  58. data/lib/spektr/targets/base.rb +119 -0
  59. data/lib/spektr/targets/config.rb +6 -0
  60. data/lib/spektr/targets/controller.rb +74 -0
  61. data/lib/spektr/targets/model.rb +6 -0
  62. data/lib/spektr/targets/routes.rb +38 -0
  63. data/lib/spektr/targets/view.rb +34 -0
  64. data/lib/spektr/version.rb +3 -0
  65. data/lib/spektr/warning.rb +23 -0
  66. data/lib/spektr.rb +120 -0
  67. data/spektr.gemspec +49 -0
  68. 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