aikido-zen 1.0.1.beta.2-arm64-linux-musl

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 (115) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +26 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +146 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +23 -0
  10. data/benchmarks/rails7.1_sql_injection.js +70 -0
  11. data/docs/banner.svg +202 -0
  12. data/docs/config.md +125 -0
  13. data/docs/rails.md +70 -0
  14. data/lib/aikido/zen/actor.rb +116 -0
  15. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  16. data/lib/aikido/zen/agent.rb +179 -0
  17. data/lib/aikido/zen/api_client.rb +142 -0
  18. data/lib/aikido/zen/attack.rb +207 -0
  19. data/lib/aikido/zen/background_worker.rb +52 -0
  20. data/lib/aikido/zen/capped_collections.rb +68 -0
  21. data/lib/aikido/zen/collector/hosts.rb +15 -0
  22. data/lib/aikido/zen/collector/routes.rb +66 -0
  23. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  24. data/lib/aikido/zen/collector/stats.rb +111 -0
  25. data/lib/aikido/zen/collector/users.rb +30 -0
  26. data/lib/aikido/zen/collector.rb +144 -0
  27. data/lib/aikido/zen/config.rb +279 -0
  28. data/lib/aikido/zen/context/rack_request.rb +24 -0
  29. data/lib/aikido/zen/context/rails_request.rb +42 -0
  30. data/lib/aikido/zen/context.rb +112 -0
  31. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  32. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  33. data/lib/aikido/zen/detached_agent/server.rb +41 -0
  34. data/lib/aikido/zen/detached_agent.rb +2 -0
  35. data/lib/aikido/zen/errors.rb +107 -0
  36. data/lib/aikido/zen/event.rb +71 -0
  37. data/lib/aikido/zen/internals.rb +102 -0
  38. data/lib/aikido/zen/libzen-v0.1.39-arm64-linux-musl.so +0 -0
  39. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
  40. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  41. data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
  42. data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
  43. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  44. data/lib/aikido/zen/outbound_connection.rb +45 -0
  45. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  46. data/lib/aikido/zen/package.rb +22 -0
  47. data/lib/aikido/zen/payload.rb +50 -0
  48. data/lib/aikido/zen/rails_engine.rb +70 -0
  49. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  50. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  51. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  52. data/lib/aikido/zen/rate_limiter.rb +50 -0
  53. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  54. data/lib/aikido/zen/request/rails_router.rb +72 -0
  55. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  56. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  57. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  58. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  59. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  60. data/lib/aikido/zen/request/schema.rb +87 -0
  61. data/lib/aikido/zen/request.rb +103 -0
  62. data/lib/aikido/zen/route.rb +39 -0
  63. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  64. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  65. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  66. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  67. data/lib/aikido/zen/runtime_settings.rb +65 -0
  68. data/lib/aikido/zen/scan.rb +75 -0
  69. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  70. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  71. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  72. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  73. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
  74. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  75. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  76. data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
  77. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
  78. data/lib/aikido/zen/scanners.rb +7 -0
  79. data/lib/aikido/zen/sink.rb +118 -0
  80. data/lib/aikido/zen/sinks/action_controller.rb +83 -0
  81. data/lib/aikido/zen/sinks/async_http.rb +82 -0
  82. data/lib/aikido/zen/sinks/curb.rb +115 -0
  83. data/lib/aikido/zen/sinks/em_http.rb +85 -0
  84. data/lib/aikido/zen/sinks/excon.rb +121 -0
  85. data/lib/aikido/zen/sinks/file.rb +116 -0
  86. data/lib/aikido/zen/sinks/http.rb +95 -0
  87. data/lib/aikido/zen/sinks/httpclient.rb +97 -0
  88. data/lib/aikido/zen/sinks/httpx.rb +80 -0
  89. data/lib/aikido/zen/sinks/kernel.rb +34 -0
  90. data/lib/aikido/zen/sinks/mysql2.rb +33 -0
  91. data/lib/aikido/zen/sinks/net_http.rb +103 -0
  92. data/lib/aikido/zen/sinks/patron.rb +105 -0
  93. data/lib/aikido/zen/sinks/pg.rb +74 -0
  94. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  95. data/lib/aikido/zen/sinks/socket.rb +80 -0
  96. data/lib/aikido/zen/sinks/sqlite3.rb +49 -0
  97. data/lib/aikido/zen/sinks/trilogy.rb +33 -0
  98. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  99. data/lib/aikido/zen/sinks.rb +39 -0
  100. data/lib/aikido/zen/sinks_dsl.rb +226 -0
  101. data/lib/aikido/zen/synchronizable.rb +24 -0
  102. data/lib/aikido/zen/system_info.rb +84 -0
  103. data/lib/aikido/zen/version.rb +10 -0
  104. data/lib/aikido/zen/worker.rb +87 -0
  105. data/lib/aikido/zen.rb +206 -0
  106. data/lib/aikido-zen.rb +3 -0
  107. data/placeholder/.gitignore +4 -0
  108. data/placeholder/README.md +11 -0
  109. data/placeholder/Rakefile +75 -0
  110. data/placeholder/lib/placeholder.rb.template +3 -0
  111. data/placeholder/placeholder.gemspec.template +20 -0
  112. data/tasklib/bench.rake +94 -0
  113. data/tasklib/libzen.rake +132 -0
  114. data/tasklib/wrk.rb +88 -0
  115. metadata +204 -0
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scanners/ssrf_scanner"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module Async
9
+ module HTTP
10
+ def self.load_sinks!
11
+ if Gem.loaded_specs["async-http"]
12
+ require "async/http"
13
+
14
+ ::Async::HTTP::Client.prepend(Async::HTTP::ClientExtensions)
15
+ end
16
+ end
17
+
18
+ SINK = Sinks.add("async-http", scanners: [
19
+ Scanners::SSRFScanner,
20
+ OutboundConnectionMonitor
21
+ ])
22
+
23
+ module Helpers
24
+ def self.scan(request, connection, operation)
25
+ SINK.scan(
26
+ request: request,
27
+ connection: connection,
28
+ operation: operation
29
+ )
30
+ end
31
+ end
32
+
33
+ module ClientExtensions
34
+ extend Sinks::DSL
35
+
36
+ sink_around :call do |super_call, request|
37
+ uri = URI(format("%<scheme>s://%<authority>s%<path>s", {
38
+ scheme: request.scheme || scheme,
39
+ authority: request.authority || authority,
40
+ path: request.path
41
+ }))
42
+
43
+ wrapped_request = Scanners::SSRFScanner::Request.new(
44
+ verb: request.method,
45
+ uri: uri,
46
+ headers: request.headers.to_h,
47
+ header_normalizer: ->(value) { Array(value).join(", ") }
48
+ )
49
+
50
+ # Store the request information so the DNS sinks can pick it up.
51
+ context = Aikido::Zen.current_context
52
+ if context
53
+ prev_request = context["ssrf.request"]
54
+ context["ssrf.request"] = wrapped_request
55
+ end
56
+
57
+ connection = OutboundConnection.from_uri(uri)
58
+
59
+ Helpers.scan(wrapped_request, connection, "request")
60
+
61
+ response = super_call.call
62
+
63
+ Scanners::SSRFScanner.track_redirects(
64
+ request: wrapped_request,
65
+ response: Scanners::SSRFScanner::Response.new(
66
+ status: response.status,
67
+ headers: response.headers.to_h,
68
+ header_normalizer: ->(value) { Array(value).join(", ") }
69
+ )
70
+ )
71
+
72
+ response
73
+ ensure
74
+ context["ssrf.request"] = prev_request if context
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ Aikido::Zen::Sinks::Async::HTTP.load_sinks!
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scanners/ssrf_scanner"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module Curl
9
+ def self.load_sinks!
10
+ if Gem.loaded_specs["curb"]
11
+ require "curb"
12
+
13
+ ::Curl::Easy.prepend(Curl::EasyExtensions)
14
+ end
15
+ end
16
+
17
+ SINK = Sinks.add("curb", scanners: [
18
+ Scanners::SSRFScanner,
19
+ OutboundConnectionMonitor
20
+ ])
21
+
22
+ module Helpers
23
+ def self.wrap_request(curl, url: curl.url)
24
+ Scanners::SSRFScanner::Request.new(
25
+ verb: nil, # Curb hides this by directly setting an option in C
26
+ uri: URI(url),
27
+ headers: curl.headers
28
+ )
29
+ end
30
+
31
+ def self.wrap_response(curl)
32
+ # Curb made an… interesting choice by not parsing the response headers
33
+ # and forcing users to do this manually if they need to look at them.
34
+ _, *headers = curl.header_str.split(/[\r\n]+/).map(&:strip)
35
+ headers = headers.flat_map { |str| str.scan(/\A(\S+): (.+)\z/) }.to_h
36
+
37
+ if curl.url != curl.last_effective_url
38
+ status = 302 # We can't know what the original status was, but we just need a 3XX
39
+ headers["Location"] = curl.last_effective_url
40
+ else
41
+ status = curl.status.to_i
42
+ end
43
+
44
+ Scanners::SSRFScanner::Response.new(status: status, headers: headers)
45
+ end
46
+
47
+ def self.scan(request, connection, operation)
48
+ SINK.scan(
49
+ request: request,
50
+ connection: connection,
51
+ operation: operation
52
+ )
53
+ end
54
+ end
55
+
56
+ module EasyExtensions
57
+ extend Sinks::DSL
58
+
59
+ sink_around :perform do |super_call|
60
+ wrapped_request = Helpers.wrap_request(self)
61
+
62
+ # Store the request information so the DNS sinks can pick it up.
63
+ context = Aikido::Zen.current_context
64
+ if context
65
+ prev_request = context["ssrf.request"]
66
+ context["ssrf.request"] = wrapped_request
67
+ end
68
+
69
+ connection = OutboundConnection.from_uri(URI(url))
70
+
71
+ Helpers.scan(wrapped_request, connection, "request")
72
+
73
+ response = super_call.call
74
+
75
+ Scanners::SSRFScanner.track_redirects(
76
+ request: wrapped_request,
77
+ response: Helpers.wrap_response(self)
78
+ )
79
+
80
+ # When libcurl has follow_location set, it will handle redirections
81
+ # internally, and expose the "last_effective_url" as the URI that was
82
+ # last requested in the redirect chain.
83
+ #
84
+ # In this case, we can't actually stop the request from happening, but
85
+ # we can scan again (now that we know another request happened), to
86
+ # stop the response from being exposed to the user. This downgrades
87
+ # the SSRF into a blind SSRF, which is better than doing nothing.
88
+ if url != last_effective_url
89
+ last_effective_request = Helpers.wrap_request(self, url: last_effective_url)
90
+
91
+ # Code coverage is disabled here because the else clause is a no-op,
92
+ # so there is nothing to cover.
93
+ # :nocov:
94
+ if context
95
+ context["ssrf.request"] = last_effective_request
96
+ else
97
+ # empty
98
+ end
99
+ # :nocov:
100
+
101
+ connection = OutboundConnection.from_uri(URI(last_effective_url))
102
+
103
+ Helpers.scan(last_effective_request, connection, "request")
104
+ end
105
+
106
+ response
107
+ ensure
108
+ context["ssrf.request"] = prev_request if context
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ Aikido::Zen::Sinks::Curl.load_sinks!
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scanners/ssrf_scanner"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module EventMachine
9
+ module HttpRequest
10
+ def self.load_sinks!
11
+ if Gem.loaded_specs["em-http-request"]
12
+ require "em-http-request"
13
+
14
+ ::EventMachine::HttpRequest.use(EventMachine::HttpRequest::Middleware)
15
+
16
+ # NOTE: We can't use middleware to intercept requests as we want to ensure any
17
+ # modifications to the request from user-supplied middleware are already applied
18
+ # before we scan the request.
19
+ ::EventMachine::HttpClient.prepend(EventMachine::HttpRequest::HttpClientExtensions)
20
+ end
21
+ end
22
+
23
+ SINK = Sinks.add("em-http-request", scanners: [
24
+ Scanners::SSRFScanner,
25
+ OutboundConnectionMonitor
26
+ ])
27
+
28
+ module Helpers
29
+ def self.scan(request, connection, operation)
30
+ SINK.scan(
31
+ request: request,
32
+ connection: connection,
33
+ operation: operation
34
+ )
35
+ end
36
+ end
37
+
38
+ module HttpClientExtensions
39
+ extend Sinks::DSL
40
+
41
+ sink_before :send_request do
42
+ wrapped_request = Scanners::SSRFScanner::Request.new(
43
+ verb: req.method.to_s,
44
+ uri: URI(req.uri),
45
+ headers: req.headers
46
+ )
47
+
48
+ # Store the request information so the DNS sinks can pick it up.
49
+ context = Aikido::Zen.current_context
50
+ context["ssrf.request"] = wrapped_request if context
51
+
52
+ connection = OutboundConnection.new(
53
+ host: req.host,
54
+ port: req.port
55
+ )
56
+
57
+ Helpers.scan(wrapped_request, connection, "request")
58
+ end
59
+ end
60
+
61
+ class Middleware
62
+ def response(client)
63
+ # Store the request information so the DNS sinks can pick it up.
64
+ context = Aikido::Zen.current_context
65
+ context["ssrf.request"] = nil if context
66
+
67
+ Scanners::SSRFScanner.track_redirects(
68
+ request: Scanners::SSRFScanner::Request.new(
69
+ verb: client.req.method,
70
+ uri: URI(client.req.uri),
71
+ headers: client.req.headers
72
+ ),
73
+ response: Scanners::SSRFScanner::Response.new(
74
+ status: client.response_header.status,
75
+ headers: client.response_header.to_h
76
+ )
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ Aikido::Zen::Sinks::EventMachine::HttpRequest.load_sinks!
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scanners/ssrf_scanner"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module Excon
9
+ def self.load_sinks!
10
+ if Gem.loaded_specs["excon"]
11
+ require "excon"
12
+
13
+ ::Excon::Connection.prepend(ConnectionExtensions)
14
+ ::Excon::Middleware::RedirectFollower.prepend(RedirectFollowerExtensions)
15
+ end
16
+ end
17
+
18
+ SINK = Sinks.add("excon", scanners: [
19
+ Scanners::SSRFScanner,
20
+ OutboundConnectionMonitor
21
+ ])
22
+
23
+ module Helpers
24
+ def self.build_request(connection, request)
25
+ uri = URI(format("%<scheme>s://%<host>s:%<port>i%<path>s", {
26
+ scheme: request.fetch(:scheme) { connection[:scheme] },
27
+ host: request.fetch(:hostname) { connection[:hostname] },
28
+ port: request.fetch(:port) { connection[:port] },
29
+ path: request.fetch(:path) { connection[:path] }
30
+ }))
31
+ uri.query = request.fetch(:query) { connection[:query] }
32
+
33
+ Scanners::SSRFScanner::Request.new(
34
+ verb: request.fetch(:method) { connection[:method] },
35
+ uri: uri,
36
+ headers: connection[:headers].to_h.merge(request[:headers].to_h)
37
+ )
38
+ end
39
+
40
+ def self.scan(request, connection, operation)
41
+ SINK.scan(
42
+ request: request,
43
+ connection: connection,
44
+ operation: operation
45
+ )
46
+ end
47
+ end
48
+
49
+ module ConnectionExtensions
50
+ extend Sinks::DSL
51
+
52
+ sink_around :request do |super_call, params = {}|
53
+ request = Helpers.build_request(@data, params)
54
+
55
+ # Store the request information so the DNS sinks can pick it up.
56
+ context = Aikido::Zen.current_context
57
+ if context
58
+ prev_request = context["ssrf.request"]
59
+ context["ssrf.request"] = request
60
+ end
61
+
62
+ connection = OutboundConnection.from_uri(request.uri)
63
+
64
+ Helpers.scan(request, connection, "request")
65
+
66
+ response = super_call.call
67
+
68
+ Scanners::SSRFScanner.track_redirects(
69
+ request: request,
70
+ response: Scanners::SSRFScanner::Response.new(
71
+ status: response.status,
72
+ headers: response.headers.to_h
73
+ )
74
+ )
75
+
76
+ response
77
+ rescue Sinks::DSL::PresafeError => err
78
+ outer_cause = err.cause
79
+ case outer_cause
80
+ when ::Excon::Error::Socket
81
+ inner_cause = outer_cause.cause
82
+ # Excon wraps errors inside the lower level layer. This only happens
83
+ # to our scanning exceptions when a request is using RedirectFollower,
84
+ # so we unwrap them when it happens so host apps can handle errors
85
+ # consistently.
86
+ raise inner_cause if inner_cause.is_a?(Aikido::Zen::UnderAttackError)
87
+ end
88
+ raise
89
+ ensure
90
+ context["ssrf.request"] = prev_request if context
91
+ end
92
+ end
93
+
94
+ module RedirectFollowerExtensions
95
+ extend Sinks::DSL
96
+
97
+ sink_before :response_call do |datum|
98
+ response = datum[:response]
99
+
100
+ # Code coverage is disabled here because the else clause is a no-op,
101
+ # so there is nothing to cover.
102
+ # :nocov:
103
+ if !response.nil?
104
+ Scanners::SSRFScanner.track_redirects(
105
+ request: Helpers.build_request(datum, {}),
106
+ response: Scanners::SSRFScanner::Response.new(
107
+ status: response[:status],
108
+ headers: response[:headers]
109
+ )
110
+ )
111
+ else
112
+ # empty
113
+ end
114
+ # :nocov:
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ Aikido::Zen::Sinks::Excon.load_sinks!
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Sinks
5
+ module File
6
+ def self.load_sinks!
7
+ # Create a copy of the original method for internal use only to prevent
8
+ # recursion in PathTraversalScanner.
9
+ #
10
+ # IMPORTANT: The alias must be created before the method is overridden,
11
+ # when the extensions are prepended.
12
+ ::File.singleton_class.alias_method(:expand_path__internal_for_aikido_zen, :expand_path)
13
+
14
+ ::File.singleton_class.prepend(FileClassExtensions)
15
+ ::File.prepend(FileExtensions)
16
+ end
17
+
18
+ SINK = Sinks.add("File", scanners: [Scanners::PathTraversalScanner])
19
+
20
+ module Helpers
21
+ def self.scan(filepath, operation)
22
+ SINK.scan(
23
+ filepath: filepath,
24
+ operation: operation
25
+ )
26
+ end
27
+ end
28
+
29
+ module FileClassExtensions
30
+ extend Sinks::DSL
31
+
32
+ sink_before :open do |path|
33
+ Helpers.scan(path, "open")
34
+ end
35
+
36
+ sink_before :read do |path|
37
+ Helpers.scan(path, "read")
38
+ end
39
+
40
+ sink_before :write do |path|
41
+ Helpers.scan(path, "write")
42
+ end
43
+
44
+ sink_before :truncate do |file_name|
45
+ Helpers.scan(file_name, "truncate")
46
+ end
47
+
48
+ sink_before :rename do |old_name, new_name|
49
+ Helpers.scan(old_name, "rename")
50
+ Helpers.scan(new_name, "rename")
51
+ end
52
+
53
+ sink_before :unlink do |*file_names|
54
+ file_names.each do |file_name|
55
+ Helpers.scan(file_name, "unlink")
56
+ end
57
+ end
58
+
59
+ sink_before :delete do |*file_names|
60
+ file_names.each do |file_name|
61
+ Helpers.scan(file_name, "delete")
62
+ end
63
+ end
64
+
65
+ sink_before :symlink do |old_name, new_name|
66
+ Helpers.scan(old_name, "symlink")
67
+ Helpers.scan(new_name, "symlink")
68
+ end
69
+
70
+ sink_before :chmod do |_mode_int, *file_names|
71
+ file_names.each do |file_name|
72
+ Helpers.scan(file_name, "chmod")
73
+ end
74
+ end
75
+
76
+ sink_before :chown do |_owner_int, group_int, *file_names|
77
+ file_names.each do |file_name|
78
+ Helpers.scan(file_name, "chown")
79
+ end
80
+ end
81
+
82
+ sink_before :utime do |_atime, _mtime, *file_names|
83
+ file_names.each do |file_name|
84
+ Helpers.scan(file_name, "utime")
85
+ end
86
+ end
87
+
88
+ sink_after :join do |result|
89
+ Helpers.scan(result, "join")
90
+ end
91
+
92
+ sink_before :expand_path do |file_name|
93
+ Helpers.scan(file_name, "expand_path")
94
+ end
95
+
96
+ sink_before :realpath do |file_name|
97
+ Helpers.scan(file_name, "realpath")
98
+ end
99
+
100
+ sink_before :realdirpath do |file_name|
101
+ Helpers.scan(file_name, "realdirpath")
102
+ end
103
+ end
104
+
105
+ module FileExtensions
106
+ extend Sinks::DSL
107
+
108
+ sink_before :initialize do |path|
109
+ Helpers.scan(path, "new")
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ Aikido::Zen::Sinks::File.load_sinks!
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scanners/ssrf_scanner"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module HTTP
9
+ def self.load_sinks!
10
+ if Gem.loaded_specs["http"]
11
+ require "http"
12
+
13
+ ::HTTP::Client.prepend(ClientExtensions)
14
+ end
15
+ end
16
+
17
+ SINK = Sinks.add("http", scanners: [
18
+ Scanners::SSRFScanner,
19
+ OutboundConnectionMonitor
20
+ ])
21
+
22
+ module Helpers
23
+ # Maps an HTTP Request to an Aikido OutboundConnection.
24
+ #
25
+ # @param req [HTTP::Request]
26
+ # @return [Aikido::Zen::OutboundConnection]
27
+ def self.build_outbound(req)
28
+ OutboundConnection.new(
29
+ host: req.socket_host,
30
+ port: req.socket_port
31
+ )
32
+ end
33
+
34
+ # Wraps the HTTP request with an API we can depend on.
35
+ #
36
+ # @param req [HTTP::Request]
37
+ # @return [Aikido::Zen::Scanners::SSRFScanner::Request]
38
+ def self.wrap_request(req)
39
+ Scanners::SSRFScanner::Request.new(
40
+ verb: req.verb,
41
+ uri: URI(req.uri.to_s),
42
+ headers: req.headers.to_h
43
+ )
44
+ end
45
+
46
+ def self.wrap_response(resp)
47
+ Scanners::SSRFScanner::Response.new(
48
+ status: resp.status,
49
+ headers: resp.headers.to_h
50
+ )
51
+ end
52
+
53
+ def self.scan(request, connection, operation)
54
+ SINK.scan(
55
+ request: request,
56
+ connection: connection,
57
+ operation: operation
58
+ )
59
+ end
60
+ end
61
+
62
+ module ClientExtensions
63
+ extend Sinks::DSL
64
+
65
+ sink_around :perform do |super_call, req|
66
+ wrapped_request = Helpers.wrap_request(req)
67
+
68
+ # Store the request information so the DNS sinks can pick it up.
69
+ context = Aikido::Zen.current_context
70
+ if context
71
+ prev_request = context["ssrf.request"]
72
+ context["ssrf.request"] = wrapped_request
73
+ end
74
+
75
+ connection = Helpers.build_outbound(req)
76
+
77
+ Helpers.scan(wrapped_request, connection, "request")
78
+
79
+ response = super_call.call
80
+
81
+ Scanners::SSRFScanner.track_redirects(
82
+ request: wrapped_request,
83
+ response: Helpers.wrap_response(response)
84
+ )
85
+
86
+ response
87
+ ensure
88
+ context["ssrf.request"] = prev_request if context
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ Aikido::Zen::Sinks::HTTP.load_sinks!
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scanners/ssrf_scanner"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module HTTPClient
9
+ def self.load_sinks!
10
+ if Gem.loaded_specs["httpclient"]
11
+ require "httpclient"
12
+
13
+ ::HTTPClient.prepend(HTTPClient::HTTPClientExtensions)
14
+ end
15
+ end
16
+
17
+ SINK = Sinks.add("httpclient", scanners: [
18
+ Scanners::SSRFScanner,
19
+ OutboundConnectionMonitor
20
+ ])
21
+
22
+ module Helpers
23
+ def self.wrap_request(req)
24
+ Scanners::SSRFScanner::Request.new(
25
+ verb: req.http_header.request_method,
26
+ uri: req.http_header.request_uri,
27
+ headers: req.headers
28
+ )
29
+ end
30
+
31
+ def self.wrap_response(resp)
32
+ # Code coverage is disabled here because `do_get_header` is not called,
33
+ # because WebMock does not mock it.
34
+ # :nocov:
35
+ Scanners::SSRFScanner::Response.new(
36
+ status: resp.http_header.status_code,
37
+ headers: resp.headers
38
+ )
39
+ # :nocov:
40
+ end
41
+
42
+ def self.scan(request, connection, operation)
43
+ SINK.scan(
44
+ request: request,
45
+ connection: connection,
46
+ operation: operation
47
+ )
48
+ end
49
+
50
+ def self.sink(req, &block)
51
+ wrapped_request = wrap_request(req)
52
+ connection = OutboundConnection.from_uri(req.http_header.request_uri)
53
+
54
+ # Store the request information so the DNS sinks can pick it up.
55
+ context = Aikido::Zen.current_context
56
+ if context
57
+ prev_request = context["ssrf.request"]
58
+ context["ssrf.request"] = wrapped_request
59
+ end
60
+
61
+ scan(wrapped_request, connection, "request")
62
+
63
+ yield
64
+ ensure
65
+ context["ssrf.request"] = prev_request if context
66
+ end
67
+ end
68
+
69
+ module HTTPClientExtensions
70
+ extend Sinks::DSL
71
+
72
+ private
73
+
74
+ sink_around :do_get_block do |super_call, req|
75
+ Helpers.sink(req, &super_call)
76
+ end
77
+
78
+ sink_around :do_get_stream do |super_call, req|
79
+ Helpers.sink(req, &super_call)
80
+ end
81
+
82
+ sink_after :do_get_header do |_result, req, res, _sess|
83
+ # Code coverage is disabled here because `do_get_header` is not called,
84
+ # because WebMock does not mock it.
85
+ # :nocov:
86
+ Scanners::SSRFScanner.track_redirects(
87
+ request: Helpers.wrap_request(req),
88
+ response: Helpers.wrap_response(res)
89
+ )
90
+ # :nocov:
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ Aikido::Zen::Sinks::HTTPClient.load_sinks!