aikido-zen 0.1.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) 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/middleware/check_allowed_addresses.rb +38 -0
  22. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  23. data/lib/aikido/zen/middleware/throttler.rb +50 -0
  24. data/lib/aikido/zen/outbound_connection.rb +45 -0
  25. data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
  26. data/lib/aikido/zen/package.rb +22 -0
  27. data/lib/aikido/zen/payload.rb +48 -0
  28. data/lib/aikido/zen/rails_engine.rb +53 -0
  29. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  30. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  31. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  32. data/lib/aikido/zen/rate_limiter.rb +55 -0
  33. data/lib/aikido/zen/request/heuristic_router.rb +109 -0
  34. data/lib/aikido/zen/request/rails_router.rb +84 -0
  35. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  36. data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
  37. data/lib/aikido/zen/request/schema/builder.rb +125 -0
  38. data/lib/aikido/zen/request/schema/definition.rb +112 -0
  39. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  40. data/lib/aikido/zen/request/schema.rb +72 -0
  41. data/lib/aikido/zen/request.rb +97 -0
  42. data/lib/aikido/zen/route.rb +39 -0
  43. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  44. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  45. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  46. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  47. data/lib/aikido/zen/runtime_settings.rb +70 -0
  48. data/lib/aikido/zen/scan.rb +75 -0
  49. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
  50. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  51. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
  52. data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
  53. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
  54. data/lib/aikido/zen/scanners.rb +5 -0
  55. data/lib/aikido/zen/sink.rb +108 -0
  56. data/lib/aikido/zen/sinks/async_http.rb +63 -0
  57. data/lib/aikido/zen/sinks/curb.rb +89 -0
  58. data/lib/aikido/zen/sinks/em_http.rb +71 -0
  59. data/lib/aikido/zen/sinks/excon.rb +103 -0
  60. data/lib/aikido/zen/sinks/http.rb +76 -0
  61. data/lib/aikido/zen/sinks/httpclient.rb +68 -0
  62. data/lib/aikido/zen/sinks/httpx.rb +61 -0
  63. data/lib/aikido/zen/sinks/mysql2.rb +21 -0
  64. data/lib/aikido/zen/sinks/net_http.rb +85 -0
  65. data/lib/aikido/zen/sinks/patron.rb +88 -0
  66. data/lib/aikido/zen/sinks/pg.rb +50 -0
  67. data/lib/aikido/zen/sinks/resolv.rb +41 -0
  68. data/lib/aikido/zen/sinks/socket.rb +51 -0
  69. data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
  70. data/lib/aikido/zen/sinks/trilogy.rb +21 -0
  71. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  72. data/lib/aikido/zen/sinks.rb +21 -0
  73. data/lib/aikido/zen/stats/routes.rb +53 -0
  74. data/lib/aikido/zen/stats/sink_stats.rb +95 -0
  75. data/lib/aikido/zen/stats/users.rb +26 -0
  76. data/lib/aikido/zen/stats.rb +171 -0
  77. data/lib/aikido/zen/synchronizable.rb +24 -0
  78. data/lib/aikido/zen/system_info.rb +84 -0
  79. data/lib/aikido/zen/version.rb +10 -0
  80. data/lib/aikido/zen.rb +138 -0
  81. data/lib/aikido-zen.rb +3 -0
  82. data/lib/aikido.rb +3 -0
  83. data/tasklib/libzen.rake +128 -0
  84. metadata +171 -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)