aikido-zen 1.0.0.pre.beta.1-x86_64-darwin → 1.0.1.beta.2-x86_64-darwin

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.aikido +6 -0
  3. data/README.md +67 -83
  4. data/lib/aikido/zen/config.rb +11 -2
  5. data/lib/aikido/zen/context.rb +4 -0
  6. data/lib/aikido/zen/internals.rb +41 -7
  7. data/lib/aikido/zen/libzen-v0.1.39-x86_64-darwin.dylib +0 -0
  8. data/lib/aikido/zen/middleware/request_tracker.rb +6 -4
  9. data/lib/aikido/zen/rails_engine.rb +5 -9
  10. data/lib/aikido/zen/request/heuristic_router.rb +6 -0
  11. data/lib/aikido/zen/sink.rb +5 -0
  12. data/lib/aikido/zen/sinks/async_http.rb +35 -16
  13. data/lib/aikido/zen/sinks/curb.rb +52 -26
  14. data/lib/aikido/zen/sinks/em_http.rb +39 -25
  15. data/lib/aikido/zen/sinks/excon.rb +63 -45
  16. data/lib/aikido/zen/sinks/file.rb +67 -71
  17. data/lib/aikido/zen/sinks/http.rb +38 -19
  18. data/lib/aikido/zen/sinks/httpclient.rb +51 -22
  19. data/lib/aikido/zen/sinks/httpx.rb +37 -18
  20. data/lib/aikido/zen/sinks/kernel.rb +18 -57
  21. data/lib/aikido/zen/sinks/mysql2.rb +19 -7
  22. data/lib/aikido/zen/sinks/net_http.rb +37 -19
  23. data/lib/aikido/zen/sinks/patron.rb +41 -24
  24. data/lib/aikido/zen/sinks/pg.rb +50 -27
  25. data/lib/aikido/zen/sinks/resolv.rb +37 -16
  26. data/lib/aikido/zen/sinks/socket.rb +33 -17
  27. data/lib/aikido/zen/sinks/sqlite3.rb +31 -12
  28. data/lib/aikido/zen/sinks/trilogy.rb +19 -7
  29. data/lib/aikido/zen/sinks.rb +29 -20
  30. data/lib/aikido/zen/sinks_dsl.rb +226 -0
  31. data/lib/aikido/zen/version.rb +2 -2
  32. data/lib/aikido/zen.rb +18 -1
  33. data/placeholder/.gitignore +4 -0
  34. data/placeholder/README.md +11 -0
  35. data/placeholder/Rakefile +75 -0
  36. data/placeholder/lib/placeholder.rb.template +3 -0
  37. data/placeholder/placeholder.gemspec.template +20 -0
  38. data/tasklib/libzen.rake +70 -66
  39. metadata +17 -13
  40. data/CHANGELOG.md +0 -25
  41. data/lib/aikido/zen/libzen-v0.1.37.x86_64.dylib +0 -0
  42. data/lib/aikido.rb +0 -3
@@ -1,19 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../sink"
3
+ require_relative "../scanners/ssrf_scanner"
4
4
  require_relative "../outbound_connection_monitor"
5
5
 
6
6
  module Aikido::Zen
7
7
  module Sinks
8
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
+
9
17
  SINK = Sinks.add("curb", scanners: [
10
- Aikido::Zen::Scanners::SSRFScanner,
11
- Aikido::Zen::OutboundConnectionMonitor
18
+ Scanners::SSRFScanner,
19
+ OutboundConnectionMonitor
12
20
  ])
13
21
 
14
- module Extensions
22
+ module Helpers
15
23
  def self.wrap_request(curl, url: curl.url)
16
- Aikido::Zen::Scanners::SSRFScanner::Request.new(
24
+ Scanners::SSRFScanner::Request.new(
17
25
  verb: nil, # Curb hides this by directly setting an option in C
18
26
  uri: URI(url),
19
27
  headers: curl.headers
@@ -33,29 +41,40 @@ module Aikido::Zen
33
41
  status = curl.status.to_i
34
42
  end
35
43
 
36
- Aikido::Zen::Scanners::SSRFScanner::Response.new(status: status, headers: headers)
44
+ Scanners::SSRFScanner::Response.new(status: status, headers: headers)
37
45
  end
38
46
 
39
- def perform
40
- wrapped_request = Extensions.wrap_request(self)
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)
41
61
 
42
62
  # Store the request information so the DNS sinks can pick it up.
43
- if (context = Aikido::Zen.current_context)
63
+ context = Aikido::Zen.current_context
64
+ if context
44
65
  prev_request = context["ssrf.request"]
45
66
  context["ssrf.request"] = wrapped_request
46
67
  end
47
68
 
48
- SINK.scan(
49
- connection: Aikido::Zen::OutboundConnection.from_uri(URI(url)),
50
- request: wrapped_request,
51
- operation: "request"
52
- )
69
+ connection = OutboundConnection.from_uri(URI(url))
53
70
 
54
- response = super
71
+ Helpers.scan(wrapped_request, connection, "request")
55
72
 
56
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
73
+ response = super_call.call
74
+
75
+ Scanners::SSRFScanner.track_redirects(
57
76
  request: wrapped_request,
58
- response: Extensions.wrap_response(self)
77
+ response: Helpers.wrap_response(self)
59
78
  )
60
79
 
61
80
  # When libcurl has follow_location set, it will handle redirections
@@ -67,14 +86,21 @@ module Aikido::Zen
67
86
  # stop the response from being exposed to the user. This downgrades
68
87
  # the SSRF into a blind SSRF, which is better than doing nothing.
69
88
  if url != last_effective_url
70
- last_effective_request = Extensions.wrap_request(self, url: last_effective_url)
71
- context["ssrf.request"] = last_effective_request if context
72
-
73
- SINK.scan(
74
- connection: Aikido::Zen::OutboundConnection.from_uri(URI(last_effective_url)),
75
- request: last_effective_request,
76
- operation: "request"
77
- )
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")
78
104
  end
79
105
 
80
106
  response
@@ -86,4 +112,4 @@ module Aikido::Zen
86
112
  end
87
113
  end
88
114
 
89
- ::Curl::Easy.prepend(Aikido::Zen::Sinks::Curl::Extensions)
115
+ Aikido::Zen::Sinks::Curl.load_sinks!
@@ -1,20 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../sink"
3
+ require_relative "../scanners/ssrf_scanner"
4
4
  require_relative "../outbound_connection_monitor"
5
5
 
6
6
  module Aikido::Zen
7
7
  module Sinks
8
8
  module EventMachine
9
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
+
10
23
  SINK = Sinks.add("em-http-request", scanners: [
11
- Aikido::Zen::Scanners::SSRFScanner,
12
- Aikido::Zen::OutboundConnectionMonitor
24
+ Scanners::SSRFScanner,
25
+ OutboundConnectionMonitor
13
26
  ])
14
27
 
15
- module Extensions
16
- def send_request(*)
17
- wrapped_request = Aikido::Zen::Scanners::SSRFScanner::Request.new(
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(
18
43
  verb: req.method.to_s,
19
44
  uri: URI(req.uri),
20
45
  headers: req.headers
@@ -24,16 +49,12 @@ module Aikido::Zen
24
49
  context = Aikido::Zen.current_context
25
50
  context["ssrf.request"] = wrapped_request if context
26
51
 
27
- SINK.scan(
28
- connection: Aikido::Zen::OutboundConnection.new(
29
- host: req.host,
30
- port: req.port
31
- ),
32
- request: wrapped_request,
33
- operation: "request"
52
+ connection = OutboundConnection.new(
53
+ host: req.host,
54
+ port: req.port
34
55
  )
35
56
 
36
- super
57
+ Helpers.scan(wrapped_request, connection, "request")
37
58
  end
38
59
  end
39
60
 
@@ -43,13 +64,13 @@ module Aikido::Zen
43
64
  context = Aikido::Zen.current_context
44
65
  context["ssrf.request"] = nil if context
45
66
 
46
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
47
- request: Aikido::Zen::Scanners::SSRFScanner::Request.new(
67
+ Scanners::SSRFScanner.track_redirects(
68
+ request: Scanners::SSRFScanner::Request.new(
48
69
  verb: client.req.method,
49
70
  uri: URI(client.req.uri),
50
71
  headers: client.req.headers
51
72
  ),
52
- response: Aikido::Zen::Scanners::SSRFScanner::Response.new(
73
+ response: Scanners::SSRFScanner::Response.new(
53
74
  status: client.response_header.status,
54
75
  headers: client.response_header.to_h
55
76
  )
@@ -61,11 +82,4 @@ module Aikido::Zen
61
82
  end
62
83
  end
63
84
 
64
- ::EventMachine::HttpRequest
65
- .use(Aikido::Zen::Sinks::EventMachine::HttpRequest::Middleware)
66
-
67
- # NOTE: We can't use middleware to intercept requests as we want to ensure any
68
- # modifications to the request from user-supplied middleware are already applied
69
- # before we scan the request.
70
- ::EventMachine::HttpClient
71
- .prepend(Aikido::Zen::Sinks::EventMachine::HttpRequest::Extensions)
85
+ Aikido::Zen::Sinks::EventMachine::HttpRequest.load_sinks!
@@ -1,31 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../sink"
3
+ require_relative "../scanners/ssrf_scanner"
4
4
  require_relative "../outbound_connection_monitor"
5
5
 
6
6
  module Aikido::Zen
7
7
  module Sinks
8
8
  module Excon
9
- SINK = Sinks.add("excon", scanners: [
10
- Aikido::Zen::Scanners::SSRFScanner,
11
- Aikido::Zen::OutboundConnectionMonitor
12
- ])
9
+ def self.load_sinks!
10
+ if Gem.loaded_specs["excon"]
11
+ require "excon"
13
12
 
14
- module Extensions
15
- # Maps Excon request params to an Aikido OutboundConnection.
16
- #
17
- # @param connection [Hash<Symbol, Object>] the data set in the connection.
18
- # @param request [Hash<Symbol, Object>] the data overrides sent for each
19
- # request.
20
- #
21
- # @return [Aikido::Zen::OutboundConnection]
22
- def self.build_outbound(connection, request)
23
- Aikido::Zen::OutboundConnection.new(
24
- host: request.fetch(:hostname) { connection[:hostname] },
25
- port: request.fetch(:port) { connection[:port] }
26
- )
13
+ ::Excon::Connection.prepend(ConnectionExtensions)
14
+ ::Excon::Middleware::RedirectFollower.prepend(RedirectFollowerExtensions)
27
15
  end
16
+ end
17
+
18
+ SINK = Sinks.add("excon", scanners: [
19
+ Scanners::SSRFScanner,
20
+ OutboundConnectionMonitor
21
+ ])
28
22
 
23
+ module Helpers
29
24
  def self.build_request(connection, request)
30
25
  uri = URI(format("%<scheme>s://%<host>s:%<port>i%<path>s", {
31
26
  scheme: request.fetch(:scheme) { connection[:scheme] },
@@ -35,45 +30,61 @@ module Aikido::Zen
35
30
  }))
36
31
  uri.query = request.fetch(:query) { connection[:query] }
37
32
 
38
- Aikido::Zen::Scanners::SSRFScanner::Request.new(
33
+ Scanners::SSRFScanner::Request.new(
39
34
  verb: request.fetch(:method) { connection[:method] },
40
35
  uri: uri,
41
36
  headers: connection[:headers].to_h.merge(request[:headers].to_h)
42
37
  )
43
38
  end
44
39
 
45
- def request(params = {}, *)
46
- request = Extensions.build_request(@data, params)
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)
47
54
 
48
55
  # Store the request information so the DNS sinks can pick it up.
49
- if (context = Aikido::Zen.current_context)
56
+ context = Aikido::Zen.current_context
57
+ if context
50
58
  prev_request = context["ssrf.request"]
51
59
  context["ssrf.request"] = request
52
60
  end
53
61
 
54
- SINK.scan(
55
- connection: Aikido::Zen::OutboundConnection.from_uri(request.uri),
56
- request: request,
57
- operation: "request"
58
- )
62
+ connection = OutboundConnection.from_uri(request.uri)
59
63
 
60
- response = super
64
+ Helpers.scan(request, connection, "request")
61
65
 
62
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
66
+ response = super_call.call
67
+
68
+ Scanners::SSRFScanner.track_redirects(
63
69
  request: request,
64
- response: Aikido::Zen::Scanners::SSRFScanner::Response.new(
70
+ response: Scanners::SSRFScanner::Response.new(
65
71
  status: response.status,
66
72
  headers: response.headers.to_h
67
73
  )
68
74
  )
69
75
 
70
76
  response
71
- rescue ::Excon::Error::Socket => err
72
- # Excon wraps errors inside the lower level layer. This only happens
73
- # to our scanning exceptions when a request is using RedirectFollower,
74
- # so we unwrap them when it happens so host apps can handle errors
75
- # consistently.
76
- raise err.cause if err.cause.is_a?(Aikido::Zen::UnderAttackError)
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
77
88
  raise
78
89
  ensure
79
90
  context["ssrf.request"] = prev_request if context
@@ -81,23 +92,30 @@ module Aikido::Zen
81
92
  end
82
93
 
83
94
  module RedirectFollowerExtensions
84
- def response_call(data)
85
- if (response = data[:response])
86
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
87
- request: Extensions.build_request(data, {}),
88
- response: Aikido::Zen::Scanners::SSRFScanner::Response.new(
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(
89
107
  status: response[:status],
90
108
  headers: response[:headers]
91
109
  )
92
110
  )
111
+ else
112
+ # empty
93
113
  end
94
-
95
- super
114
+ # :nocov:
96
115
  end
97
116
  end
98
117
  end
99
118
  end
100
119
  end
101
120
 
102
- ::Excon::Connection.prepend(Aikido::Zen::Sinks::Excon::Extensions)
103
- ::Excon::Middleware::RedirectFollower.prepend(Aikido::Zen::Sinks::Excon::RedirectFollowerExtensions)
121
+ Aikido::Zen::Sinks::Excon.load_sinks!
@@ -3,118 +3,114 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
5
  module File
6
- SINK = Sinks.add("File", scanners: [
7
- Aikido::Zen::Scanners::PathTraversalScanner
8
- ])
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])
9
19
 
10
- module Extensions
11
- def self.scan_path(filepath, operation)
20
+ module Helpers
21
+ def self.scan(filepath, operation)
12
22
  SINK.scan(
13
23
  filepath: filepath,
14
24
  operation: operation
15
25
  )
16
26
  end
27
+ end
17
28
 
18
- # Module to extend only the initializer method of `File` (`File.new`)
19
- module Initiliazer
20
- def initialize(filename, *, **)
21
- Extensions.scan_path(filename, "new")
22
- super
23
- end
24
- end
29
+ module FileClassExtensions
30
+ extend Sinks::DSL
25
31
 
26
- def open(filename, *, **)
27
- Extensions.scan_path(filename, "open")
28
- super
32
+ sink_before :open do |path|
33
+ Helpers.scan(path, "open")
29
34
  end
30
35
 
31
- def read(filename, *)
32
- Extensions.scan_path(filename, "read")
33
- super
36
+ sink_before :read do |path|
37
+ Helpers.scan(path, "read")
34
38
  end
35
39
 
36
- def write(filename, *, **)
37
- Extensions.scan_path(filename, "write")
38
- super
40
+ sink_before :write do |path|
41
+ Helpers.scan(path, "write")
39
42
  end
40
43
 
41
- def join(*)
42
- joined = super
43
- Extensions.scan_path(joined, "join")
44
- joined
44
+ sink_before :truncate do |file_name|
45
+ Helpers.scan(file_name, "truncate")
45
46
  end
46
47
 
47
- def chmod(mode, *paths)
48
- paths.each { |path| Extensions.scan_path(path, "chmod") }
49
- super
48
+ sink_before :rename do |old_name, new_name|
49
+ Helpers.scan(old_name, "rename")
50
+ Helpers.scan(new_name, "rename")
50
51
  end
51
52
 
52
- def chown(user, group, *paths)
53
- paths.each { |path| Extensions.scan_path(path, "chown") }
54
- super
53
+ sink_before :unlink do |*file_names|
54
+ file_names.each do |file_name|
55
+ Helpers.scan(file_name, "unlink")
56
+ end
55
57
  end
56
58
 
57
- def rename(from, to)
58
- Extensions.scan_path(from, "rename")
59
- Extensions.scan_path(to, "rename")
60
- super
59
+ sink_before :delete do |*file_names|
60
+ file_names.each do |file_name|
61
+ Helpers.scan(file_name, "delete")
62
+ end
61
63
  end
62
64
 
63
- def symlink(from, to)
64
- Extensions.scan_path(from, "symlink")
65
- Extensions.scan_path(to, "symlink")
66
- super
65
+ sink_before :symlink do |old_name, new_name|
66
+ Helpers.scan(old_name, "symlink")
67
+ Helpers.scan(new_name, "symlink")
67
68
  end
68
69
 
69
- def truncate(file_name, *)
70
- Extensions.scan_path(file_name, "truncate")
71
- super
70
+ sink_before :chmod do |_mode_int, *file_names|
71
+ file_names.each do |file_name|
72
+ Helpers.scan(file_name, "chmod")
73
+ end
72
74
  end
73
75
 
74
- def unlink(*args)
75
- args.each do |arg|
76
- Extensions.scan_path(arg, "unlink")
76
+ sink_before :chown do |_owner_int, group_int, *file_names|
77
+ file_names.each do |file_name|
78
+ Helpers.scan(file_name, "chown")
77
79
  end
78
- super
79
80
  end
80
81
 
81
- def delete(*args)
82
- args.each do |arg|
83
- Extensions.scan_path(arg, "delete")
82
+ sink_before :utime do |_atime, _mtime, *file_names|
83
+ file_names.each do |file_name|
84
+ Helpers.scan(file_name, "utime")
84
85
  end
85
- super
86
86
  end
87
87
 
88
- def utime(atime, mtime, *args)
89
- args.each do |arg|
90
- Extensions.scan_path(arg, "utime")
91
- end
92
- super
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")
93
94
  end
94
95
 
95
- def expand_path(filename, *)
96
- Extensions.scan_path(filename, "expand_path")
97
- super
96
+ sink_before :realpath do |file_name|
97
+ Helpers.scan(file_name, "realpath")
98
98
  end
99
99
 
100
- def realpath(filename, *)
101
- Extensions.scan_path(filename, "realpath")
102
- super
100
+ sink_before :realdirpath do |file_name|
101
+ Helpers.scan(file_name, "realdirpath")
103
102
  end
103
+ end
104
+
105
+ module FileExtensions
106
+ extend Sinks::DSL
104
107
 
105
- def realdirpath(filename, *)
106
- Extensions.scan_path(filename, "realdirpath")
107
- super
108
+ sink_before :initialize do |path|
109
+ Helpers.scan(path, "new")
108
110
  end
109
111
  end
110
112
  end
111
113
  end
112
114
  end
113
115
 
114
- # Internally, Path Traversal's scanner logic uses `expand_path`, in order to avoid recursion issues we keep
115
- # a copy of the original method, only to be used internally.
116
- # It's important to keep this line before prepend the Extensions module, otherwise the alias will call
117
- # the extended method.
118
- ::File.singleton_class.alias_method :expand_path__internal_for_aikido_zen, :expand_path
119
- ::File.singleton_class.prepend(Aikido::Zen::Sinks::File::Extensions)
120
- ::File.prepend Aikido::Zen::Sinks::File::Extensions::Initiliazer
116
+ Aikido::Zen::Sinks::File.load_sinks!
@@ -1,23 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../sink"
3
+ require_relative "../scanners/ssrf_scanner"
4
4
  require_relative "../outbound_connection_monitor"
5
5
 
6
6
  module Aikido::Zen
7
7
  module Sinks
8
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
+
9
17
  SINK = Sinks.add("http", scanners: [
10
- Aikido::Zen::Scanners::SSRFScanner,
11
- Aikido::Zen::OutboundConnectionMonitor
18
+ Scanners::SSRFScanner,
19
+ OutboundConnectionMonitor
12
20
  ])
13
21
 
14
- module Extensions
22
+ module Helpers
15
23
  # Maps an HTTP Request to an Aikido OutboundConnection.
16
24
  #
17
25
  # @param req [HTTP::Request]
18
26
  # @return [Aikido::Zen::OutboundConnection]
19
27
  def self.build_outbound(req)
20
- Aikido::Zen::OutboundConnection.new(
28
+ OutboundConnection.new(
21
29
  host: req.socket_host,
22
30
  port: req.socket_port
23
31
  )
@@ -28,7 +36,7 @@ module Aikido::Zen
28
36
  # @param req [HTTP::Request]
29
37
  # @return [Aikido::Zen::Scanners::SSRFScanner::Request]
30
38
  def self.wrap_request(req)
31
- Aikido::Zen::Scanners::SSRFScanner::Request.new(
39
+ Scanners::SSRFScanner::Request.new(
32
40
  verb: req.verb,
33
41
  uri: URI(req.uri.to_s),
34
42
  headers: req.headers.to_h
@@ -36,32 +44,43 @@ module Aikido::Zen
36
44
  end
37
45
 
38
46
  def self.wrap_response(resp)
39
- Aikido::Zen::Scanners::SSRFScanner::Response.new(
47
+ Scanners::SSRFScanner::Response.new(
40
48
  status: resp.status,
41
49
  headers: resp.headers.to_h
42
50
  )
43
51
  end
44
52
 
45
- def perform(req, *)
46
- wrapped_request = Extensions.wrap_request(req)
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)
47
67
 
48
68
  # Store the request information so the DNS sinks can pick it up.
49
- if (context = Aikido::Zen.current_context)
69
+ context = Aikido::Zen.current_context
70
+ if context
50
71
  prev_request = context["ssrf.request"]
51
72
  context["ssrf.request"] = wrapped_request
52
73
  end
53
74
 
54
- SINK.scan(
55
- request: wrapped_request,
56
- connection: Extensions.build_outbound(req),
57
- operation: "request"
58
- )
75
+ connection = Helpers.build_outbound(req)
76
+
77
+ Helpers.scan(wrapped_request, connection, "request")
59
78
 
60
- response = super
79
+ response = super_call.call
61
80
 
62
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
81
+ Scanners::SSRFScanner.track_redirects(
63
82
  request: wrapped_request,
64
- response: Extensions.wrap_response(response)
83
+ response: Helpers.wrap_response(response)
65
84
  )
66
85
 
67
86
  response
@@ -73,4 +92,4 @@ module Aikido::Zen
73
92
  end
74
93
  end
75
94
 
76
- ::HTTP::Client.prepend(Aikido::Zen::Sinks::HTTP::Extensions)
95
+ Aikido::Zen::Sinks::HTTP.load_sinks!