aikido-zen 1.0.2-aarch64-linux

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 (125) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +32 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +148 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +22 -0
  10. data/benchmarks/rails7.1_benchmark.js +1 -0
  11. data/benchmarks/rails7.1_sql_injection.js +102 -0
  12. data/docs/banner.svg +202 -0
  13. data/docs/config.md +133 -0
  14. data/docs/proxy.md +10 -0
  15. data/docs/rails.md +112 -0
  16. data/docs/troubleshooting.md +62 -0
  17. data/lib/aikido/zen/actor.rb +146 -0
  18. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  19. data/lib/aikido/zen/agent.rb +181 -0
  20. data/lib/aikido/zen/api_client.rb +145 -0
  21. data/lib/aikido/zen/attack.rb +217 -0
  22. data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
  23. data/lib/aikido/zen/attack_wave.rb +88 -0
  24. data/lib/aikido/zen/background_worker.rb +52 -0
  25. data/lib/aikido/zen/cache.rb +91 -0
  26. data/lib/aikido/zen/capped_collections.rb +86 -0
  27. data/lib/aikido/zen/collector/event.rb +238 -0
  28. data/lib/aikido/zen/collector/hosts.rb +30 -0
  29. data/lib/aikido/zen/collector/routes.rb +71 -0
  30. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  31. data/lib/aikido/zen/collector/stats.rb +122 -0
  32. data/lib/aikido/zen/collector/users.rb +32 -0
  33. data/lib/aikido/zen/collector.rb +223 -0
  34. data/lib/aikido/zen/config.rb +312 -0
  35. data/lib/aikido/zen/context/rack_request.rb +27 -0
  36. data/lib/aikido/zen/context/rails_request.rb +47 -0
  37. data/lib/aikido/zen/context.rb +145 -0
  38. data/lib/aikido/zen/detached_agent/agent.rb +79 -0
  39. data/lib/aikido/zen/detached_agent/front_object.rb +41 -0
  40. data/lib/aikido/zen/detached_agent/server.rb +78 -0
  41. data/lib/aikido/zen/detached_agent.rb +2 -0
  42. data/lib/aikido/zen/errors.rb +107 -0
  43. data/lib/aikido/zen/event.rb +116 -0
  44. data/lib/aikido/zen/helpers.rb +24 -0
  45. data/lib/aikido/zen/internals.rb +123 -0
  46. data/lib/aikido/zen/libzen-v0.1.48-aarch64-linux.so +0 -0
  47. data/lib/aikido/zen/middleware/allowed_address_checker.rb +26 -0
  48. data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
  49. data/lib/aikido/zen/middleware/context_setter.rb +26 -0
  50. data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
  51. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  52. data/lib/aikido/zen/middleware/rack_throttler.rb +50 -0
  53. data/lib/aikido/zen/middleware/request_tracker.rb +197 -0
  54. data/lib/aikido/zen/outbound_connection.rb +62 -0
  55. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  56. data/lib/aikido/zen/package.rb +22 -0
  57. data/lib/aikido/zen/payload.rb +50 -0
  58. data/lib/aikido/zen/rails_engine.rb +53 -0
  59. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  60. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  61. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  62. data/lib/aikido/zen/rate_limiter.rb +50 -0
  63. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  64. data/lib/aikido/zen/request/rails_router.rb +92 -0
  65. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  66. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  67. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  68. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  69. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  70. data/lib/aikido/zen/request/schema.rb +87 -0
  71. data/lib/aikido/zen/request.rb +88 -0
  72. data/lib/aikido/zen/route.rb +96 -0
  73. data/lib/aikido/zen/runtime_settings/endpoints.rb +78 -0
  74. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  75. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  76. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  77. data/lib/aikido/zen/runtime_settings.rb +66 -0
  78. data/lib/aikido/zen/scan.rb +75 -0
  79. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +68 -0
  80. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +64 -0
  81. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  82. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +65 -0
  83. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +94 -0
  84. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  85. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  86. data/lib/aikido/zen/scanners/ssrf_scanner.rb +266 -0
  87. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +55 -0
  88. data/lib/aikido/zen/scanners.rb +7 -0
  89. data/lib/aikido/zen/sink.rb +118 -0
  90. data/lib/aikido/zen/sinks/action_controller.rb +85 -0
  91. data/lib/aikido/zen/sinks/async_http.rb +80 -0
  92. data/lib/aikido/zen/sinks/curb.rb +113 -0
  93. data/lib/aikido/zen/sinks/em_http.rb +83 -0
  94. data/lib/aikido/zen/sinks/excon.rb +118 -0
  95. data/lib/aikido/zen/sinks/file.rb +153 -0
  96. data/lib/aikido/zen/sinks/http.rb +93 -0
  97. data/lib/aikido/zen/sinks/httpclient.rb +95 -0
  98. data/lib/aikido/zen/sinks/httpx.rb +78 -0
  99. data/lib/aikido/zen/sinks/kernel.rb +33 -0
  100. data/lib/aikido/zen/sinks/mysql2.rb +31 -0
  101. data/lib/aikido/zen/sinks/net_http.rb +101 -0
  102. data/lib/aikido/zen/sinks/patron.rb +103 -0
  103. data/lib/aikido/zen/sinks/pg.rb +72 -0
  104. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  105. data/lib/aikido/zen/sinks/socket.rb +85 -0
  106. data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
  107. data/lib/aikido/zen/sinks/trilogy.rb +31 -0
  108. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  109. data/lib/aikido/zen/sinks.rb +36 -0
  110. data/lib/aikido/zen/sinks_dsl.rb +250 -0
  111. data/lib/aikido/zen/synchronizable.rb +24 -0
  112. data/lib/aikido/zen/system_info.rb +80 -0
  113. data/lib/aikido/zen/version.rb +10 -0
  114. data/lib/aikido/zen/worker.rb +87 -0
  115. data/lib/aikido/zen.rb +303 -0
  116. data/lib/aikido-zen.rb +3 -0
  117. data/placeholder/.gitignore +4 -0
  118. data/placeholder/README.md +11 -0
  119. data/placeholder/Rakefile +75 -0
  120. data/placeholder/lib/placeholder.rb.template +3 -0
  121. data/placeholder/placeholder.gemspec.template +20 -0
  122. data/tasklib/bench.rake +94 -0
  123. data/tasklib/libzen.rake +133 -0
  124. data/tasklib/wrk.rb +88 -0
  125. metadata +214 -0
@@ -0,0 +1,113 @@
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
+ SINK = Sinks.add("curb", scanners: [
10
+ Scanners::SSRFScanner,
11
+ OutboundConnectionMonitor
12
+ ])
13
+
14
+ module Helpers
15
+ def self.wrap_request(curl, url: curl.url)
16
+ Scanners::SSRFScanner::Request.new(
17
+ verb: nil, # Curb hides this by directly setting an option in C
18
+ uri: URI(url),
19
+ headers: curl.headers
20
+ )
21
+ end
22
+
23
+ def self.wrap_response(curl)
24
+ # Curb made an… interesting choice by not parsing the response headers
25
+ # and forcing users to do this manually if they need to look at them.
26
+ _, *headers = curl.header_str.split(/[\r\n]+/).map(&:strip)
27
+ headers = headers.flat_map { |str| str.scan(/\A(\S+): (.+)\z/) }.to_h
28
+
29
+ if curl.url != curl.last_effective_url
30
+ status = 302 # We can't know what the original status was, but we just need a 3XX
31
+ headers["Location"] = curl.last_effective_url
32
+ else
33
+ status = curl.status.to_i
34
+ end
35
+
36
+ Scanners::SSRFScanner::Response.new(status: status, headers: headers)
37
+ end
38
+
39
+ def self.scan(request, connection, operation)
40
+ SINK.scan(
41
+ request: request,
42
+ connection: connection,
43
+ operation: operation
44
+ )
45
+ end
46
+ end
47
+
48
+ def self.load_sinks!
49
+ if Aikido::Zen.satisfy "curb", ">= 0.2.3"
50
+ require "curb"
51
+
52
+ ::Curl::Easy.class_eval do
53
+ extend Sinks::DSL
54
+
55
+ sink_around :perform do |original_call|
56
+ wrapped_request = Helpers.wrap_request(self)
57
+
58
+ # Store the request information so the DNS sinks can pick it up.
59
+ context = Aikido::Zen.current_context
60
+ if context
61
+ prev_request = context["ssrf.request"]
62
+ context["ssrf.request"] = wrapped_request
63
+ end
64
+
65
+ connection = OutboundConnection.from_uri(URI(url))
66
+
67
+ Helpers.scan(wrapped_request, connection, "request")
68
+
69
+ response = original_call.call
70
+
71
+ Scanners::SSRFScanner.track_redirects(
72
+ request: wrapped_request,
73
+ response: Helpers.wrap_response(self)
74
+ )
75
+
76
+ # When libcurl has follow_location set, it will handle redirections
77
+ # internally, and expose the "last_effective_url" as the URI that was
78
+ # last requested in the redirect chain.
79
+ #
80
+ # In this case, we can't actually stop the request from happening, but
81
+ # we can scan again (now that we know another request happened), to
82
+ # stop the response from being exposed to the user. This downgrades
83
+ # the SSRF into a blind SSRF, which is better than doing nothing.
84
+ if url != last_effective_url
85
+ last_effective_request = Helpers.wrap_request(self, url: last_effective_url)
86
+
87
+ # Code coverage is disabled here because the else clause is a no-op,
88
+ # so there is nothing to cover.
89
+ # :nocov:
90
+ if context
91
+ context["ssrf.request"] = last_effective_request
92
+ else
93
+ # empty
94
+ end
95
+ # :nocov:
96
+
97
+ connection = OutboundConnection.from_uri(URI(last_effective_url))
98
+
99
+ Helpers.scan(last_effective_request, connection, "request")
100
+ end
101
+
102
+ response
103
+ ensure
104
+ context["ssrf.request"] = prev_request if context
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ Aikido::Zen::Sinks::Curl.load_sinks!
@@ -0,0 +1,83 @@
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
+ SINK = Sinks.add("em-http-request", scanners: [
11
+ Scanners::SSRFScanner,
12
+ OutboundConnectionMonitor
13
+ ])
14
+
15
+ module Helpers
16
+ def self.scan(request, connection, operation)
17
+ SINK.scan(
18
+ request: request,
19
+ connection: connection,
20
+ operation: operation
21
+ )
22
+ end
23
+ end
24
+
25
+ def self.load_sinks!
26
+ if Aikido::Zen.satisfy "em-http-request", ">= 1.0"
27
+ require "em-http-request"
28
+
29
+ ::EventMachine::HttpRequest.use(EventMachine::HttpRequest::Middleware)
30
+
31
+ # NOTE: We can't use middleware to intercept requests as we want to ensure any
32
+ # modifications to the request from user-supplied middleware are already applied
33
+ # before we scan the request.
34
+ ::EventMachine::HttpClient.class_eval do
35
+ extend Sinks::DSL
36
+
37
+ sink_before :send_request do
38
+ wrapped_request = Scanners::SSRFScanner::Request.new(
39
+ verb: req.method.to_s,
40
+ uri: URI(req.uri),
41
+ headers: req.headers
42
+ )
43
+
44
+ # Store the request information so the DNS sinks can pick it up.
45
+ context = Aikido::Zen.current_context
46
+ context["ssrf.request"] = wrapped_request if context
47
+
48
+ connection = OutboundConnection.new(
49
+ host: req.host,
50
+ port: req.port
51
+ )
52
+
53
+ Helpers.scan(wrapped_request, connection, "request")
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ class Middleware
60
+ def response(client)
61
+ # Store the request information so the DNS sinks can pick it up.
62
+ context = Aikido::Zen.current_context
63
+ context["ssrf.request"] = nil if context
64
+
65
+ Scanners::SSRFScanner.track_redirects(
66
+ request: Scanners::SSRFScanner::Request.new(
67
+ verb: client.req.method,
68
+ uri: URI(client.req.uri),
69
+ headers: client.req.headers
70
+ ),
71
+ response: Scanners::SSRFScanner::Response.new(
72
+ status: client.response_header.status,
73
+ headers: client.response_header.to_h
74
+ )
75
+ )
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ Aikido::Zen::Sinks::EventMachine::HttpRequest.load_sinks!
@@ -0,0 +1,118 @@
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
+ SINK = Sinks.add("excon", scanners: [
10
+ Scanners::SSRFScanner,
11
+ OutboundConnectionMonitor
12
+ ])
13
+
14
+ module Helpers
15
+ def self.build_request(connection, request)
16
+ uri = URI(format("%<scheme>s://%<host>s:%<port>i%<path>s", {
17
+ scheme: request.fetch(:scheme) { connection[:scheme] },
18
+ host: request.fetch(:hostname) { connection[:hostname] },
19
+ port: request.fetch(:port) { connection[:port] },
20
+ path: request.fetch(:path) { connection[:path] }
21
+ }))
22
+ uri.query = request.fetch(:query) { connection[:query] }
23
+
24
+ Scanners::SSRFScanner::Request.new(
25
+ verb: request.fetch(:method) { connection[:method] },
26
+ uri: uri,
27
+ headers: connection[:headers].to_h.merge(request[:headers].to_h)
28
+ )
29
+ end
30
+
31
+ def self.scan(request, connection, operation)
32
+ SINK.scan(
33
+ request: request,
34
+ connection: connection,
35
+ operation: operation
36
+ )
37
+ end
38
+ end
39
+
40
+ def self.load_sinks!
41
+ if Aikido::Zen.satisfy "excon", ">= 0.50.0"
42
+ require "excon"
43
+
44
+ ::Excon::Connection.class_eval do
45
+ extend Sinks::DSL
46
+
47
+ sink_around :request do |original_call, params = {}|
48
+ request = Helpers.build_request(@data, params)
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"] = request
55
+ end
56
+
57
+ connection = OutboundConnection.from_uri(request.uri)
58
+
59
+ Helpers.scan(request, connection, "request")
60
+
61
+ response = original_call.call
62
+
63
+ Scanners::SSRFScanner.track_redirects(
64
+ request: request,
65
+ response: Scanners::SSRFScanner::Response.new(
66
+ status: response.status,
67
+ headers: response.headers.to_h
68
+ )
69
+ )
70
+
71
+ response
72
+ rescue Sinks::DSL::PresafeError => err
73
+ outer_cause = err.cause
74
+ case outer_cause
75
+ when ::Excon::Error::Socket
76
+ inner_cause = outer_cause.cause
77
+ # Excon wraps errors inside the lower level layer. This only happens
78
+ # to our scanning exceptions when a request is using RedirectFollower,
79
+ # so we unwrap them when it happens so host apps can handle errors
80
+ # consistently.
81
+ raise inner_cause if inner_cause.is_a?(Aikido::Zen::UnderAttackError)
82
+ end
83
+ raise
84
+ ensure
85
+ context["ssrf.request"] = prev_request if context
86
+ end
87
+ end
88
+
89
+ ::Excon::Middleware::RedirectFollower.class_eval do
90
+ extend Sinks::DSL
91
+
92
+ sink_before :response_call do |datum|
93
+ response = datum[:response]
94
+
95
+ # Code coverage is disabled here because the else clause is a no-op,
96
+ # so there is nothing to cover.
97
+ # :nocov:
98
+ if !response.nil?
99
+ Scanners::SSRFScanner.track_redirects(
100
+ request: Helpers.build_request(datum, {}),
101
+ response: Scanners::SSRFScanner::Response.new(
102
+ status: response[:status],
103
+ headers: response[:headers]
104
+ )
105
+ )
106
+ else
107
+ # empty
108
+ end
109
+ # :nocov:
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ Aikido::Zen::Sinks::Excon.load_sinks!
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Sinks
5
+ module File
6
+ SINK = Sinks.add("File", scanners: [Scanners::PathTraversalScanner])
7
+
8
+ module Helpers
9
+ def self.scan(filepath, operation)
10
+ SINK.scan(
11
+ filepath: filepath,
12
+ operation: operation
13
+ )
14
+ end
15
+ end
16
+
17
+ def self.load_sinks!
18
+ ::File.singleton_class.class_eval do
19
+ extend Sinks::DSL
20
+
21
+ # Create a copy of the original methods for internal use only to prevent
22
+ # recursion in PathTraversalScanner.
23
+ #
24
+ # IMPORTANT: The aliases must be created before the method is overridden.
25
+ alias_method :expand_path__internal_for_aikido_zen, :expand_path
26
+ alias_method :join__internal_for_aikido_zen, :join
27
+
28
+ sink_before :open do |path|
29
+ Helpers.scan(path, "open")
30
+ end
31
+
32
+ sink_before :read do |path|
33
+ Helpers.scan(path, "read")
34
+ end
35
+
36
+ sink_before :write do |path|
37
+ Helpers.scan(path, "write")
38
+ end
39
+
40
+ sink_before :truncate do |file_name|
41
+ Helpers.scan(file_name, "truncate")
42
+ end
43
+
44
+ sink_before :rename do |old_name, new_name|
45
+ Helpers.scan(old_name, "rename")
46
+ Helpers.scan(new_name, "rename")
47
+ end
48
+
49
+ sink_before :unlink do |*file_names|
50
+ file_names.each do |file_name|
51
+ Helpers.scan(file_name, "unlink")
52
+ end
53
+ end
54
+
55
+ sink_before :delete do |*file_names|
56
+ file_names.each do |file_name|
57
+ Helpers.scan(file_name, "delete")
58
+ end
59
+ end
60
+
61
+ sink_before :symlink do |old_name, new_name|
62
+ Helpers.scan(old_name, "symlink")
63
+ Helpers.scan(new_name, "symlink")
64
+ end
65
+
66
+ sink_before :chmod do |_mode_int, *file_names|
67
+ file_names.each do |file_name|
68
+ Helpers.scan(file_name, "chmod")
69
+ end
70
+ end
71
+
72
+ sink_before :chown do |_owner_int, group_int, *file_names|
73
+ file_names.each do |file_name|
74
+ Helpers.scan(file_name, "chown")
75
+ end
76
+ end
77
+
78
+ sink_before :utime do |_atime, _mtime, *file_names|
79
+ file_names.each do |file_name|
80
+ Helpers.scan(file_name, "utime")
81
+ end
82
+ end
83
+
84
+ def join(*args, **kwargs, &blk)
85
+ if Aikido::Zen.config.harden?
86
+ # IMPORTANT: THE BEHAVIOR OF THIS METHOD IS CHANGED!
87
+ #
88
+ # File.join has undocumented behavior:
89
+ #
90
+ # File.join recursively joins nested string arrays.
91
+ #
92
+ # This prevents path traversal detection when an array originates
93
+ # from user input that was assumed to be a string.
94
+ #
95
+ # This undocumented behavior has been restricted to support path
96
+ # traversal detection.
97
+ #
98
+ # File.join no longer joins nested string arrays, but still accepts
99
+ # a single string array argument.
100
+
101
+ # File.join is often incorrectly called with a single array argument.
102
+ #
103
+ # i.e.
104
+ #
105
+ # File.join(["prefix", "filename"])
106
+ #
107
+ # This is considered acceptable.
108
+ #
109
+ # Calling File.join with a single string argument returns the string
110
+ # argument itself, having no practical effect. Therefore, it can be
111
+ # presumed that if File.join is called with a single array argument
112
+ # then this was its intended usage, and the array did not originate
113
+ # from user input that was assumed to be a string.
114
+ strings = args
115
+ strings = args.first if args.size == 1 && args.first.is_a?(Array)
116
+ strings.each do |string|
117
+ raise TypeError.new("Zen prevented implicit conversion of Array to String in hardened method. Visit https://github.com/AikidoSec/firewall-ruby for more information.") if string.is_a?(Array)
118
+ end
119
+ end
120
+
121
+ result = join__internal_for_aikido_zen(*args, **kwargs, &blk)
122
+ Sinks::DSL.safe do
123
+ Helpers.scan(result, "join")
124
+ end
125
+ result
126
+ end
127
+
128
+ sink_before :expand_path do |file_name|
129
+ Helpers.scan(file_name, "expand_path")
130
+ end
131
+
132
+ sink_before :realpath do |file_name|
133
+ Helpers.scan(file_name, "realpath")
134
+ end
135
+
136
+ sink_before :realdirpath do |file_name|
137
+ Helpers.scan(file_name, "realdirpath")
138
+ end
139
+ end
140
+
141
+ ::File.class_eval do
142
+ extend Sinks::DSL
143
+
144
+ sink_before :initialize do |path|
145
+ Helpers.scan(path, "new")
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ Aikido::Zen::Sinks::File.load_sinks!
@@ -0,0 +1,93 @@
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
+ SINK = Sinks.add("http", scanners: [
10
+ Scanners::SSRFScanner,
11
+ OutboundConnectionMonitor
12
+ ])
13
+
14
+ module Helpers
15
+ # Maps an HTTP Request to an Aikido OutboundConnection.
16
+ #
17
+ # @param req [HTTP::Request]
18
+ # @return [Aikido::Zen::OutboundConnection]
19
+ def self.build_outbound(req)
20
+ OutboundConnection.new(
21
+ host: req.socket_host,
22
+ port: req.socket_port
23
+ )
24
+ end
25
+
26
+ # Wraps the HTTP request with an API we can depend on.
27
+ #
28
+ # @param req [HTTP::Request]
29
+ # @return [Aikido::Zen::Scanners::SSRFScanner::Request]
30
+ def self.wrap_request(req)
31
+ Scanners::SSRFScanner::Request.new(
32
+ verb: req.verb,
33
+ uri: URI(req.uri.to_s),
34
+ headers: req.headers.to_h
35
+ )
36
+ end
37
+
38
+ def self.wrap_response(resp)
39
+ Scanners::SSRFScanner::Response.new(
40
+ status: resp.status,
41
+ headers: resp.headers.to_h
42
+ )
43
+ end
44
+
45
+ def self.scan(request, connection, operation)
46
+ SINK.scan(
47
+ request: request,
48
+ connection: connection,
49
+ operation: operation
50
+ )
51
+ end
52
+ end
53
+
54
+ def self.load_sinks!
55
+ if Aikido::Zen.satisfy "http", ">= 1.0"
56
+ require "http"
57
+
58
+ ::HTTP::Client.class_eval do
59
+ extend Sinks::DSL
60
+
61
+ sink_around :perform do |original_call, req|
62
+ wrapped_request = Helpers.wrap_request(req)
63
+
64
+ # Store the request information so the DNS sinks can pick it up.
65
+ context = Aikido::Zen.current_context
66
+ if context
67
+ prev_request = context["ssrf.request"]
68
+ context["ssrf.request"] = wrapped_request
69
+ end
70
+
71
+ connection = Helpers.build_outbound(req)
72
+
73
+ Helpers.scan(wrapped_request, connection, "request")
74
+
75
+ response = original_call.call
76
+
77
+ Scanners::SSRFScanner.track_redirects(
78
+ request: wrapped_request,
79
+ response: Helpers.wrap_response(response)
80
+ )
81
+
82
+ response
83
+ ensure
84
+ context["ssrf.request"] = prev_request if context
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ Aikido::Zen::Sinks::HTTP.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 HTTPClient
9
+ SINK = Sinks.add("httpclient", scanners: [
10
+ Scanners::SSRFScanner,
11
+ OutboundConnectionMonitor
12
+ ])
13
+
14
+ module Helpers
15
+ def self.wrap_request(req)
16
+ Scanners::SSRFScanner::Request.new(
17
+ verb: req.http_header.request_method,
18
+ uri: req.http_header.request_uri,
19
+ headers: req.headers
20
+ )
21
+ end
22
+
23
+ def self.wrap_response(resp)
24
+ # Code coverage is disabled here because `do_get_header` is not called,
25
+ # because WebMock does not mock it.
26
+ # :nocov:
27
+ Scanners::SSRFScanner::Response.new(
28
+ status: resp.http_header.status_code,
29
+ headers: resp.headers
30
+ )
31
+ # :nocov:
32
+ end
33
+
34
+ def self.scan(request, connection, operation)
35
+ SINK.scan(
36
+ request: request,
37
+ connection: connection,
38
+ operation: operation
39
+ )
40
+ end
41
+
42
+ def self.sink(req, &block)
43
+ wrapped_request = wrap_request(req)
44
+ connection = OutboundConnection.from_uri(req.http_header.request_uri)
45
+
46
+ # Store the request information so the DNS sinks can pick it up.
47
+ context = Aikido::Zen.current_context
48
+ if context
49
+ prev_request = context["ssrf.request"]
50
+ context["ssrf.request"] = wrapped_request
51
+ end
52
+
53
+ scan(wrapped_request, connection, "request")
54
+
55
+ yield
56
+ ensure
57
+ context["ssrf.request"] = prev_request if context
58
+ end
59
+ end
60
+
61
+ def self.load_sinks!
62
+ if Aikido::Zen.satisfy "httpclient", ">= 2.0"
63
+ require "httpclient"
64
+
65
+ ::HTTPClient.class_eval do
66
+ extend Sinks::DSL
67
+
68
+ private
69
+
70
+ sink_around :do_get_block do |original_call, req|
71
+ Helpers.sink(req, &original_call)
72
+ end
73
+
74
+ sink_around :do_get_stream do |original_call, req|
75
+ Helpers.sink(req, &original_call)
76
+ end
77
+
78
+ sink_after :do_get_header do |_result, req, res, _sess|
79
+ # Code coverage is disabled here because `do_get_header` is not called,
80
+ # because WebMock does not mock it.
81
+ # :nocov:
82
+ Scanners::SSRFScanner.track_redirects(
83
+ request: Helpers.wrap_request(req),
84
+ response: Helpers.wrap_response(res)
85
+ )
86
+ # :nocov:
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ Aikido::Zen::Sinks::HTTPClient.load_sinks!