spektr 0.1.0

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