aikido-zen 0.1.0.alpha4-arm64-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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/.standard.yml +3 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE +674 -0
  7. data/README.md +40 -0
  8. data/Rakefile +63 -0
  9. data/lib/aikido/zen/actor.rb +116 -0
  10. data/lib/aikido/zen/agent.rb +187 -0
  11. data/lib/aikido/zen/api_client.rb +132 -0
  12. data/lib/aikido/zen/attack.rb +138 -0
  13. data/lib/aikido/zen/capped_collections.rb +68 -0
  14. data/lib/aikido/zen/config.rb +229 -0
  15. data/lib/aikido/zen/context/rack_request.rb +24 -0
  16. data/lib/aikido/zen/context/rails_request.rb +42 -0
  17. data/lib/aikido/zen/context.rb +101 -0
  18. data/lib/aikido/zen/errors.rb +88 -0
  19. data/lib/aikido/zen/event.rb +66 -0
  20. data/lib/aikido/zen/internals.rb +64 -0
  21. data/lib/aikido/zen/libzen-v0.1.26.aarch64.dylib +0 -0
  22. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
  23. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  24. data/lib/aikido/zen/middleware/throttler.rb +50 -0
  25. data/lib/aikido/zen/outbound_connection.rb +45 -0
  26. data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
  27. data/lib/aikido/zen/package.rb +22 -0
  28. data/lib/aikido/zen/payload.rb +48 -0
  29. data/lib/aikido/zen/rails_engine.rb +53 -0
  30. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  31. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  32. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  33. data/lib/aikido/zen/rate_limiter.rb +55 -0
  34. data/lib/aikido/zen/request/heuristic_router.rb +109 -0
  35. data/lib/aikido/zen/request/rails_router.rb +84 -0
  36. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  37. data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
  38. data/lib/aikido/zen/request/schema/builder.rb +125 -0
  39. data/lib/aikido/zen/request/schema/definition.rb +112 -0
  40. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  41. data/lib/aikido/zen/request/schema.rb +72 -0
  42. data/lib/aikido/zen/request.rb +97 -0
  43. data/lib/aikido/zen/route.rb +39 -0
  44. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  45. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  46. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  47. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  48. data/lib/aikido/zen/runtime_settings.rb +70 -0
  49. data/lib/aikido/zen/scan.rb +75 -0
  50. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
  51. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  52. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
  53. data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
  54. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
  55. data/lib/aikido/zen/scanners.rb +5 -0
  56. data/lib/aikido/zen/sink.rb +108 -0
  57. data/lib/aikido/zen/sinks/async_http.rb +63 -0
  58. data/lib/aikido/zen/sinks/curb.rb +89 -0
  59. data/lib/aikido/zen/sinks/em_http.rb +71 -0
  60. data/lib/aikido/zen/sinks/excon.rb +103 -0
  61. data/lib/aikido/zen/sinks/http.rb +76 -0
  62. data/lib/aikido/zen/sinks/httpclient.rb +68 -0
  63. data/lib/aikido/zen/sinks/httpx.rb +61 -0
  64. data/lib/aikido/zen/sinks/mysql2.rb +21 -0
  65. data/lib/aikido/zen/sinks/net_http.rb +85 -0
  66. data/lib/aikido/zen/sinks/patron.rb +88 -0
  67. data/lib/aikido/zen/sinks/pg.rb +50 -0
  68. data/lib/aikido/zen/sinks/resolv.rb +41 -0
  69. data/lib/aikido/zen/sinks/socket.rb +51 -0
  70. data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
  71. data/lib/aikido/zen/sinks/trilogy.rb +21 -0
  72. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  73. data/lib/aikido/zen/sinks.rb +21 -0
  74. data/lib/aikido/zen/stats/routes.rb +53 -0
  75. data/lib/aikido/zen/stats/sink_stats.rb +95 -0
  76. data/lib/aikido/zen/stats/users.rb +26 -0
  77. data/lib/aikido/zen/stats.rb +171 -0
  78. data/lib/aikido/zen/synchronizable.rb +24 -0
  79. data/lib/aikido/zen/system_info.rb +84 -0
  80. data/lib/aikido/zen/version.rb +10 -0
  81. data/lib/aikido/zen.rb +138 -0
  82. data/lib/aikido-zen.rb +3 -0
  83. data/lib/aikido.rb +3 -0
  84. data/tasklib/libzen.rake +128 -0
  85. metadata +175 -0
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module Excon
9
+ SINK = Sinks.add("excon", scanners: [
10
+ Aikido::Zen::Scanners::SSRFScanner,
11
+ Aikido::Zen::OutboundConnectionMonitor
12
+ ])
13
+
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
+ )
27
+ end
28
+
29
+ def self.build_request(connection, request)
30
+ uri = URI(format("%<scheme>s://%<host>s:%<port>i%<path>s", {
31
+ scheme: request.fetch(:scheme) { connection[:scheme] },
32
+ host: request.fetch(:hostname) { connection[:hostname] },
33
+ port: request.fetch(:port) { connection[:port] },
34
+ path: request.fetch(:path) { connection[:path] }
35
+ }))
36
+ uri.query = request.fetch(:query) { connection[:query] }
37
+
38
+ Aikido::Zen::Scanners::SSRFScanner::Request.new(
39
+ verb: request.fetch(:method) { connection[:method] },
40
+ uri: uri,
41
+ headers: connection[:headers].to_h.merge(request[:headers].to_h)
42
+ )
43
+ end
44
+
45
+ def request(params = {}, *)
46
+ request = Extensions.build_request(@data, params)
47
+
48
+ # Store the request information so the DNS sinks can pick it up.
49
+ if (context = Aikido::Zen.current_context)
50
+ prev_request = context["ssrf.request"]
51
+ context["ssrf.request"] = request
52
+ end
53
+
54
+ SINK.scan(
55
+ connection: Aikido::Zen::OutboundConnection.from_uri(request.uri),
56
+ request: request,
57
+ operation: "request"
58
+ )
59
+
60
+ response = super
61
+
62
+ Aikido::Zen::Scanners::SSRFScanner.track_redirects(
63
+ request: request,
64
+ response: Aikido::Zen::Scanners::SSRFScanner::Response.new(
65
+ status: response.status,
66
+ headers: response.headers.to_h
67
+ )
68
+ )
69
+
70
+ 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
+ raise
78
+ ensure
79
+ context["ssrf.request"] = prev_request if context
80
+ end
81
+ end
82
+
83
+ 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(
89
+ status: response[:status],
90
+ headers: response[:headers]
91
+ )
92
+ )
93
+ end
94
+
95
+ super
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ ::Excon::Connection.prepend(Aikido::Zen::Sinks::Excon::Extensions)
103
+ ::Excon::Middleware::RedirectFollower.prepend(Aikido::Zen::Sinks::Excon::RedirectFollowerExtensions)
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module HTTP
9
+ SINK = Sinks.add("http", scanners: [
10
+ Aikido::Zen::Scanners::SSRFScanner,
11
+ Aikido::Zen::OutboundConnectionMonitor
12
+ ])
13
+
14
+ module Extensions
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
+ Aikido::Zen::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
+ Aikido::Zen::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
+ Aikido::Zen::Scanners::SSRFScanner::Response.new(
40
+ status: resp.status,
41
+ headers: resp.headers.to_h
42
+ )
43
+ end
44
+
45
+ def perform(req, *)
46
+ wrapped_request = Extensions.wrap_request(req)
47
+
48
+ # Store the request information so the DNS sinks can pick it up.
49
+ if (context = Aikido::Zen.current_context)
50
+ prev_request = context["ssrf.request"]
51
+ context["ssrf.request"] = wrapped_request
52
+ end
53
+
54
+ SINK.scan(
55
+ request: wrapped_request,
56
+ connection: Extensions.build_outbound(req),
57
+ operation: "request"
58
+ )
59
+
60
+ response = super
61
+
62
+ Aikido::Zen::Scanners::SSRFScanner.track_redirects(
63
+ request: wrapped_request,
64
+ response: Extensions.wrap_response(response)
65
+ )
66
+
67
+ response
68
+ ensure
69
+ context["ssrf.request"] = prev_request
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ ::HTTP::Client.prepend(Aikido::Zen::Sinks::HTTP::Extensions)
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module HTTPClient
9
+ SINK = Sinks.add("httpclient", scanners: [
10
+ Aikido::Zen::Scanners::SSRFScanner,
11
+ Aikido::Zen::OutboundConnectionMonitor
12
+ ])
13
+
14
+ module Extensions
15
+ def self.wrap_request(req)
16
+ Aikido::Zen::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
+ Aikido::Zen::Scanners::SSRFScanner::Response.new(
25
+ status: resp.http_header.status_code,
26
+ headers: resp.headers
27
+ )
28
+ end
29
+
30
+ def self.perform_scan(req, &block)
31
+ wrapped_request = wrap_request(req)
32
+ connection = Aikido::Zen::OutboundConnection.from_uri(req.http_header.request_uri)
33
+
34
+ # Store the request information so the DNS sinks can pick it up.
35
+ if (context = Aikido::Zen.current_context)
36
+ prev_request = context["ssrf.request"]
37
+ context["ssrf.request"] = wrapped_request
38
+ end
39
+
40
+ SINK.scan(connection: connection, request: wrapped_request, operation: "request")
41
+
42
+ yield
43
+ ensure
44
+ context["ssrf.request"] = prev_request if context
45
+ end
46
+
47
+ def do_get_block(req, *)
48
+ Extensions.perform_scan(req) { super }
49
+ end
50
+
51
+ def do_get_stream(req, *)
52
+ Extensions.perform_scan(req) { super }
53
+ end
54
+
55
+ def do_get_header(req, res, *)
56
+ super.tap do
57
+ Aikido::Zen::Scanners::SSRFScanner.track_redirects(
58
+ request: Extensions.wrap_request(req),
59
+ response: Extensions.wrap_response(res)
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ ::HTTPClient.prepend(Aikido::Zen::Sinks::HTTPClient::Extensions)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module HTTPX
9
+ SINK = Sinks.add("httpx", scanners: [
10
+ Aikido::Zen::Scanners::SSRFScanner,
11
+ Aikido::Zen::OutboundConnectionMonitor
12
+ ])
13
+
14
+ module Extensions
15
+ def self.wrap_request(request)
16
+ Aikido::Zen::Scanners::SSRFScanner::Request.new(
17
+ verb: request.verb,
18
+ uri: request.uri,
19
+ headers: request.headers.to_hash
20
+ )
21
+ end
22
+
23
+ def self.wrap_response(response)
24
+ Aikido::Zen::Scanners::SSRFScanner::Response.new(
25
+ status: response.status,
26
+ headers: response.headers.to_hash
27
+ )
28
+ end
29
+
30
+ def send_request(request, *)
31
+ wrapped_request = Extensions.wrap_request(request)
32
+
33
+ # Store the request information so the DNS sinks can pick it up.
34
+ if (context = Aikido::Zen.current_context)
35
+ prev_request = context["ssrf.request"]
36
+ context["ssrf.request"] = wrapped_request
37
+ end
38
+
39
+ SINK.scan(
40
+ connection: Aikido::Zen::OutboundConnection.from_uri(request.uri),
41
+ request: wrapped_request,
42
+ operation: "request"
43
+ )
44
+
45
+ request.on(:response) do |response|
46
+ Aikido::Zen::Scanners::SSRFScanner.track_redirects(
47
+ request: wrapped_request,
48
+ response: Extensions.wrap_response(response)
49
+ )
50
+ end
51
+
52
+ super
53
+ ensure
54
+ context["ssrf.request"] = prev_request if context
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ ::HTTPX::Session.prepend(Aikido::Zen::Sinks::HTTPX::Extensions)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+
5
+ module Aikido::Zen
6
+ module Sinks
7
+ module Mysql2
8
+ SINK = Sinks.add("mysql2", scanners: [Scanners::SQLInjectionScanner])
9
+
10
+ module Extensions
11
+ def query(query, *)
12
+ SINK.scan(query: query, dialect: :mysql, operation: "query")
13
+
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ ::Mysql2::Client.prepend(Aikido::Zen::Sinks::Mysql2::Extensions)
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module Net
9
+ module HTTP
10
+ SINK = Sinks.add("net-http", scanners: [
11
+ Aikido::Zen::Scanners::SSRFScanner,
12
+ Aikido::Zen::OutboundConnectionMonitor
13
+ ])
14
+
15
+ module Extensions
16
+ # Maps a Net::HTTP connection to an Aikido OutboundConnection,
17
+ # which our tooling expects.
18
+ #
19
+ # @param http [Net::HTTP]
20
+ # @return [Aikido::Zen::OutboundConnection]
21
+ def self.build_outbound(http)
22
+ Aikido::Zen::OutboundConnection.new(
23
+ host: http.address,
24
+ port: http.port
25
+ )
26
+ end
27
+
28
+ def self.wrap_request(req, session)
29
+ uri = req.uri if req.uri.is_a?(URI)
30
+ uri ||= URI(format("%<scheme>s://%<hostname>s:%<port>s%<path>s", {
31
+ scheme: session.use_ssl? ? "https" : "http",
32
+ hostname: session.address,
33
+ port: session.port,
34
+ path: req.path
35
+ }))
36
+
37
+ Aikido::Zen::Scanners::SSRFScanner::Request.new(
38
+ verb: req.method,
39
+ uri: uri,
40
+ headers: req.to_hash,
41
+ header_normalizer: ->(val) { Array(val).join(", ") }
42
+ )
43
+ end
44
+
45
+ def self.wrap_response(response)
46
+ Aikido::Zen::Scanners::SSRFScanner::Response.new(
47
+ status: response.code.to_i,
48
+ headers: response.to_hash,
49
+ header_normalizer: ->(val) { Array(val).join(", ") }
50
+ )
51
+ end
52
+
53
+ def request(req, *)
54
+ wrapped_request = Extensions.wrap_request(req, self)
55
+
56
+ # Store the request information so the DNS sinks can pick it up.
57
+ if (context = Aikido::Zen.current_context)
58
+ prev_request = context["ssrf.request"]
59
+ context["ssrf.request"] = wrapped_request
60
+ end
61
+
62
+ SINK.scan(
63
+ connection: Extensions.build_outbound(self),
64
+ request: wrapped_request,
65
+ operation: "request"
66
+ )
67
+
68
+ response = super
69
+
70
+ Aikido::Zen::Scanners::SSRFScanner.track_redirects(
71
+ request: wrapped_request,
72
+ response: Extensions.wrap_response(response)
73
+ )
74
+
75
+ response
76
+ ensure
77
+ context["ssrf.request"] = prev_request if context
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ ::Net::HTTP.prepend(Aikido::Zen::Sinks::Net::HTTP::Extensions)
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module Patron
9
+ SINK = Sinks.add("patron", scanners: [
10
+ Aikido::Zen::Scanners::SSRFScanner,
11
+ Aikido::Zen::OutboundConnectionMonitor
12
+ ])
13
+
14
+ module Extensions
15
+ def self.wrap_response(request, response)
16
+ # In this case, automatic redirection happened inside libcurl.
17
+ if response.url != request.url && !response.url.to_s.empty?
18
+ Aikido::Zen::Scanners::SSRFScanner::Response.new(
19
+ status: 302, # We can't know what the actual status was, but we just need a 3XX
20
+ headers: response.headers.merge("Location" => response.url)
21
+ )
22
+ else
23
+ Aikido::Zen::Scanners::SSRFScanner::Response.new(
24
+ status: response.status,
25
+ headers: response.headers
26
+ )
27
+ end
28
+ end
29
+
30
+ def handle_request(request)
31
+ wrapped_request = Aikido::Zen::Scanners::SSRFScanner::Request.new(
32
+ verb: request.action,
33
+ uri: URI(request.url),
34
+ headers: request.headers
35
+ )
36
+
37
+ # Store the request information so the DNS sinks can pick it up.
38
+ if (context = Aikido::Zen.current_context)
39
+ prev_request = context["ssrf.request"]
40
+ context["ssrf.request"] = wrapped_request
41
+ end
42
+
43
+ SINK.scan(
44
+ connection: Aikido::Zen::OutboundConnection.from_uri(URI(request.url)),
45
+ request: wrapped_request,
46
+ operation: "request"
47
+ )
48
+
49
+ response = super
50
+
51
+ Aikido::Zen::Scanners::SSRFScanner.track_redirects(
52
+ request: wrapped_request,
53
+ response: Extensions.wrap_response(request, response)
54
+ )
55
+
56
+ # When libcurl has follow_location set, it will handle redirections
57
+ # internally, and expose the response.url as the URI that was last
58
+ # requested in the redirect chain.
59
+ #
60
+ # In this case, we can't actually stop the request from happening, but
61
+ # we can scan again (now that we know another request happened), to
62
+ # stop the response from being exposed to the user. This downgrades
63
+ # the SSRF into a blind SSRF, which is better than doing nothing.
64
+ if request.url != response.url && !response.url.to_s.empty?
65
+ last_effective_request = Aikido::Zen::Scanners::SSRFScanner::Request.new(
66
+ verb: request.action,
67
+ uri: URI(response.url),
68
+ headers: request.headers
69
+ )
70
+ context["ssrf.request"] = last_effective_request if context
71
+
72
+ SINK.scan(
73
+ connection: Aikido::Zen::OutboundConnection.from_uri(URI(response.url)),
74
+ request: last_effective_request,
75
+ operation: "request"
76
+ )
77
+ end
78
+
79
+ response
80
+ ensure
81
+ context["ssrf.request"] = prev_request if context
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ ::Patron::Session.prepend(Aikido::Zen::Sinks::Patron::Extensions)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+
5
+ module Aikido::Zen
6
+ module Sinks
7
+ module PG
8
+ SINK = Sinks.add("pg", scanners: [Scanners::SQLInjectionScanner])
9
+
10
+ module Extensions
11
+ %i[
12
+ send_query exec sync_exec async_exec
13
+ send_query_params exec_params sync_exec_params async_exec_params
14
+ ].each do |method|
15
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
16
+ def #{method}(query, *)
17
+ SINK.scan(query: query, dialect: :postgresql, operation: :#{method})
18
+ super
19
+ rescue Aikido::Zen::SQLInjectionError
20
+ # The pg adapter does not wrap exceptions in StatementInvalid, which
21
+ # leads to inconsistent handling. This guarantees that all Aikido
22
+ # errors are wrapped in a StatementInvalid, so documentation can be
23
+ # consistent.
24
+ raise ActiveRecord::StatementInvalid
25
+ end
26
+ RUBY
27
+ end
28
+
29
+ %i[
30
+ send_prepare prepare async_prepare sync_prepare
31
+ ].each do |method|
32
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
33
+ def #{method}(_, query, *)
34
+ SINK.scan(query: query, dialect: :postgresql, operation: :#{method})
35
+ super
36
+ rescue Aikido::Zen::SQLInjectionError
37
+ # The pg adapter does not wrap exceptions in StatementInvalid, which
38
+ # leads to inconsistent handling. This guarantees that all Aikido
39
+ # errors are wrapped in a StatementInvalid, so documentation can be
40
+ # consistent.
41
+ raise ActiveRecord::StatementInvalid
42
+ end
43
+ RUBY
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ ::PG::Connection.prepend(Aikido::Zen::Sinks::PG::Extensions)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+ require_relative "../scanners/stored_ssrf_scanner"
5
+ require_relative "../scanners/ssrf_scanner"
6
+
7
+ module Aikido::Zen
8
+ module Sinks
9
+ module Resolv
10
+ SINK = Sinks.add("resolv", scanners: [
11
+ Aikido::Zen::Scanners::StoredSSRFScanner,
12
+ Aikido::Zen::Scanners::SSRFScanner
13
+ ])
14
+
15
+ module Extensions
16
+ def each_address(name, &block)
17
+ addresses = []
18
+
19
+ super do |address|
20
+ addresses << address
21
+ yield address
22
+ end
23
+ ensure
24
+ if (context = Aikido::Zen.current_context)
25
+ context["dns.lookups"] ||= Aikido::Zen::Scanners::SSRF::DNSLookups.new
26
+ context["dns.lookups"].add(name, addresses)
27
+ end
28
+
29
+ SINK.scan(
30
+ hostname: name,
31
+ addresses: addresses,
32
+ request: context && context["ssrf.request"],
33
+ operation: "lookup"
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ ::Resolv.prepend(Aikido::Zen::Sinks::Resolv::Extensions)
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Aikido::Zen
6
+ module Sinks
7
+ # We intercept IPSocket.open to hook our DNS checks around it, since
8
+ # there's no way to access the internal DNS resolution that happens in C
9
+ # when using the socket primitives.
10
+ module Socket
11
+ SINK = Sinks.add("socket", scanners: [
12
+ Aikido::Zen::Scanners::StoredSSRFScanner,
13
+ Aikido::Zen::Scanners::SSRFScanner
14
+ ])
15
+
16
+ module IPSocketExtensions
17
+ def self.scan_socket(hostname, socket)
18
+ # ["AF_INET", 80, "10.0.0.1", "10.0.0.1"]
19
+ addr_family, *, remote_address = socket.peeraddr
20
+
21
+ # We only care about IPv4 (AF_INET) or IPv6 (AF_INET6) sockets
22
+ # This might be overcautious, since this is _IP_Socket, so you
23
+ # would expect it's only used for IP connections?
24
+ return unless addr_family.start_with?("AF_INET")
25
+
26
+ if (context = Aikido::Zen.current_context)
27
+ context["dns.lookups"] ||= Aikido::Zen::Scanners::SSRF::DNSLookups.new
28
+ context["dns.lookups"].add(hostname, remote_address)
29
+ end
30
+
31
+ SINK.scan(
32
+ hostname: hostname,
33
+ addresses: [remote_address],
34
+ request: context && context["ssrf.request"],
35
+ operation: "open"
36
+ )
37
+ end
38
+
39
+ def open(name, *)
40
+ socket = super
41
+
42
+ IPSocketExtensions.scan_socket(name, socket)
43
+
44
+ socket
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ ::IPSocket.singleton_class.prepend(Aikido::Zen::Sinks::Socket::IPSocketExtensions)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+
5
+ module Aikido::Zen
6
+ module Sinks
7
+ module SQLite3
8
+ SINK = Sinks.add("sqlite3", scanners: [Scanners::SQLInjectionScanner])
9
+
10
+ module DatabaseExt
11
+ def exec_batch(sql, *)
12
+ SINK.scan(query: sql, dialect: :sqlite, operation: "exec_batch")
13
+
14
+ super
15
+ end
16
+ end
17
+
18
+ module StatementExt
19
+ def initialize(_, sql, *)
20
+ SINK.scan(query: sql, dialect: :sqlite, operation: "statement.execute")
21
+
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ ::SQLite3::Database.prepend(Aikido::Zen::Sinks::SQLite3::DatabaseExt)
30
+ ::SQLite3::Statement.prepend(Aikido::Zen::Sinks::SQLite3::StatementExt)