aikido-zen 1.0.2.beta.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 (116) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +26 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +146 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +23 -0
  10. data/benchmarks/rails7.1_sql_injection.js +70 -0
  11. data/docs/banner.svg +202 -0
  12. data/docs/config.md +125 -0
  13. data/docs/proxy.md +10 -0
  14. data/docs/rails.md +114 -0
  15. data/lib/aikido/zen/actor.rb +116 -0
  16. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  17. data/lib/aikido/zen/agent.rb +179 -0
  18. data/lib/aikido/zen/api_client.rb +145 -0
  19. data/lib/aikido/zen/attack.rb +207 -0
  20. data/lib/aikido/zen/background_worker.rb +52 -0
  21. data/lib/aikido/zen/capped_collections.rb +68 -0
  22. data/lib/aikido/zen/collector/hosts.rb +15 -0
  23. data/lib/aikido/zen/collector/routes.rb +66 -0
  24. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  25. data/lib/aikido/zen/collector/stats.rb +111 -0
  26. data/lib/aikido/zen/collector/users.rb +30 -0
  27. data/lib/aikido/zen/collector.rb +144 -0
  28. data/lib/aikido/zen/config.rb +282 -0
  29. data/lib/aikido/zen/context/rack_request.rb +24 -0
  30. data/lib/aikido/zen/context/rails_request.rb +44 -0
  31. data/lib/aikido/zen/context.rb +112 -0
  32. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  33. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  34. data/lib/aikido/zen/detached_agent/server.rb +78 -0
  35. data/lib/aikido/zen/detached_agent.rb +2 -0
  36. data/lib/aikido/zen/errors.rb +107 -0
  37. data/lib/aikido/zen/event.rb +71 -0
  38. data/lib/aikido/zen/internals.rb +103 -0
  39. data/lib/aikido/zen/libzen-v0.1.39-aarch64-linux.so +0 -0
  40. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
  41. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  42. data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
  43. data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
  44. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  45. data/lib/aikido/zen/outbound_connection.rb +45 -0
  46. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  47. data/lib/aikido/zen/package.rb +22 -0
  48. data/lib/aikido/zen/payload.rb +50 -0
  49. data/lib/aikido/zen/rails_engine.rb +56 -0
  50. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  51. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  52. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  53. data/lib/aikido/zen/rate_limiter.rb +50 -0
  54. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  55. data/lib/aikido/zen/request/rails_router.rb +77 -0
  56. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  57. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  58. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  59. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  60. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  61. data/lib/aikido/zen/request/schema.rb +87 -0
  62. data/lib/aikido/zen/request.rb +122 -0
  63. data/lib/aikido/zen/route.rb +39 -0
  64. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  65. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  66. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  67. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  68. data/lib/aikido/zen/runtime_settings.rb +65 -0
  69. data/lib/aikido/zen/scan.rb +75 -0
  70. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  71. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  72. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  73. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  74. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
  75. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  76. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  77. data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
  78. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
  79. data/lib/aikido/zen/scanners.rb +7 -0
  80. data/lib/aikido/zen/sink.rb +118 -0
  81. data/lib/aikido/zen/sinks/action_controller.rb +83 -0
  82. data/lib/aikido/zen/sinks/async_http.rb +80 -0
  83. data/lib/aikido/zen/sinks/curb.rb +113 -0
  84. data/lib/aikido/zen/sinks/em_http.rb +83 -0
  85. data/lib/aikido/zen/sinks/excon.rb +118 -0
  86. data/lib/aikido/zen/sinks/file.rb +112 -0
  87. data/lib/aikido/zen/sinks/http.rb +93 -0
  88. data/lib/aikido/zen/sinks/httpclient.rb +95 -0
  89. data/lib/aikido/zen/sinks/httpx.rb +78 -0
  90. data/lib/aikido/zen/sinks/kernel.rb +33 -0
  91. data/lib/aikido/zen/sinks/mysql2.rb +31 -0
  92. data/lib/aikido/zen/sinks/net_http.rb +101 -0
  93. data/lib/aikido/zen/sinks/patron.rb +103 -0
  94. data/lib/aikido/zen/sinks/pg.rb +72 -0
  95. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  96. data/lib/aikido/zen/sinks/socket.rb +78 -0
  97. data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
  98. data/lib/aikido/zen/sinks/trilogy.rb +31 -0
  99. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  100. data/lib/aikido/zen/sinks.rb +36 -0
  101. data/lib/aikido/zen/sinks_dsl.rb +250 -0
  102. data/lib/aikido/zen/synchronizable.rb +24 -0
  103. data/lib/aikido/zen/system_info.rb +84 -0
  104. data/lib/aikido/zen/version.rb +10 -0
  105. data/lib/aikido/zen/worker.rb +87 -0
  106. data/lib/aikido/zen.rb +246 -0
  107. data/lib/aikido-zen.rb +3 -0
  108. data/placeholder/.gitignore +4 -0
  109. data/placeholder/README.md +11 -0
  110. data/placeholder/Rakefile +75 -0
  111. data/placeholder/lib/placeholder.rb.template +3 -0
  112. data/placeholder/placeholder.gemspec.template +20 -0
  113. data/tasklib/bench.rake +94 -0
  114. data/tasklib/libzen.rake +133 -0
  115. data/tasklib/wrk.rb +88 -0
  116. metadata +205 -0
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Sinks
5
+ module ActionController
6
+ # Implements the "middleware" for blocking requests (i.e.: rate-limiting or blocking
7
+ # user/bots) in Rails apps, where we need to check at the end of the `before_action`
8
+ # chain, rather than in an actual Rack middleware, to allow for calls to
9
+ # `Zen.track_user` being made from before_actions in the host app, thus allowing
10
+ # block/rate-limit by user ID rather than solely by IP.
11
+ class BlockRequestChecker
12
+ def initialize(
13
+ config: Aikido::Zen.config,
14
+ settings: Aikido::Zen.runtime_settings,
15
+ detached_agent: Aikido::Zen.detached_agent
16
+ )
17
+ @config = config
18
+ @settings = settings
19
+ @detached_agent = detached_agent
20
+ end
21
+
22
+ def block?(controller)
23
+ context = controller.request.env[Aikido::Zen::ENV_KEY]
24
+ request = context.request
25
+
26
+ if should_block_user?(request)
27
+ status, headers, body = @config.blocked_responder.call(request, :user)
28
+ controller.headers.update(headers)
29
+ controller.render plain: Array(body).join, status: status
30
+
31
+ return true
32
+ end
33
+
34
+ if should_throttle?(request)
35
+ status, headers, body = @config.rate_limited_responder.call(request)
36
+ controller.headers.update(headers)
37
+ controller.render plain: Array(body).join, status: status
38
+
39
+ return true
40
+ end
41
+
42
+ false
43
+ end
44
+
45
+ private def should_throttle?(request)
46
+ return false unless @settings.endpoints[request.route].rate_limiting.enabled?
47
+ return false if @settings.skip_protection_for_ips.include?(request.ip)
48
+
49
+ result = @detached_agent.calculate_rate_limits(request)
50
+ return false unless result
51
+
52
+ request.env["aikido.rate_limiting"] = result
53
+ request.env["aikido.rate_limiting"].throttled?
54
+ end
55
+
56
+ # @param request [Aikido::Zen::Request]
57
+ private def should_block_user?(request)
58
+ return false if request.actor.nil?
59
+
60
+ Aikido::Zen.runtime_settings.blocked_user_ids&.include?(request.actor.id)
61
+ end
62
+ end
63
+
64
+ def self.block_request_checker
65
+ @block_request_checker ||= Aikido::Zen::Sinks::ActionController::BlockRequestChecker.new
66
+ end
67
+
68
+ module Extensions
69
+ def run_callbacks(kind, *)
70
+ return super unless kind == :process_action
71
+
72
+ super do
73
+ checker = Aikido::Zen::Sinks::ActionController.block_request_checker
74
+
75
+ yield if block_given? && !checker.block?(self)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ ::AbstractController::Callbacks.prepend(Aikido::Zen::Sinks::ActionController::Extensions)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scanners/ssrf_scanner"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module Async
9
+ module HTTP
10
+ SINK = Sinks.add("async-http", 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 "async-http", ">= 0.70.0"
27
+ require "async/http"
28
+
29
+ ::Async::HTTP::Client.class_eval do
30
+ extend Sinks::DSL
31
+
32
+ sink_around :call do |original_call, request|
33
+ uri = URI(format("%<scheme>s://%<authority>s%<path>s", {
34
+ scheme: request.scheme || scheme,
35
+ authority: request.authority || authority,
36
+ path: request.path
37
+ }))
38
+
39
+ wrapped_request = Scanners::SSRFScanner::Request.new(
40
+ verb: request.method,
41
+ uri: uri,
42
+ headers: request.headers.to_h,
43
+ header_normalizer: ->(value) { Array(value).join(", ") }
44
+ )
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
+ connection = OutboundConnection.from_uri(uri)
54
+
55
+ Helpers.scan(wrapped_request, connection, "request")
56
+
57
+ response = original_call.call
58
+
59
+ Scanners::SSRFScanner.track_redirects(
60
+ request: wrapped_request,
61
+ response: Scanners::SSRFScanner::Response.new(
62
+ status: response.status,
63
+ headers: response.headers.to_h,
64
+ header_normalizer: ->(value) { Array(value).join(", ") }
65
+ )
66
+ )
67
+
68
+ response
69
+ ensure
70
+ context["ssrf.request"] = prev_request if context
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ Aikido::Zen::Sinks::Async::HTTP.load_sinks!
@@ -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,112 @@
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 method for internal use only to prevent
22
+ # recursion in PathTraversalScanner.
23
+ #
24
+ # IMPORTANT: The alias must be created before the method is overridden.
25
+ alias_method :expand_path__internal_for_aikido_zen, :expand_path
26
+
27
+ sink_before :open do |path|
28
+ Helpers.scan(path, "open")
29
+ end
30
+
31
+ sink_before :read do |path|
32
+ Helpers.scan(path, "read")
33
+ end
34
+
35
+ sink_before :write do |path|
36
+ Helpers.scan(path, "write")
37
+ end
38
+
39
+ sink_before :truncate do |file_name|
40
+ Helpers.scan(file_name, "truncate")
41
+ end
42
+
43
+ sink_before :rename do |old_name, new_name|
44
+ Helpers.scan(old_name, "rename")
45
+ Helpers.scan(new_name, "rename")
46
+ end
47
+
48
+ sink_before :unlink do |*file_names|
49
+ file_names.each do |file_name|
50
+ Helpers.scan(file_name, "unlink")
51
+ end
52
+ end
53
+
54
+ sink_before :delete do |*file_names|
55
+ file_names.each do |file_name|
56
+ Helpers.scan(file_name, "delete")
57
+ end
58
+ end
59
+
60
+ sink_before :symlink do |old_name, new_name|
61
+ Helpers.scan(old_name, "symlink")
62
+ Helpers.scan(new_name, "symlink")
63
+ end
64
+
65
+ sink_before :chmod do |_mode_int, *file_names|
66
+ file_names.each do |file_name|
67
+ Helpers.scan(file_name, "chmod")
68
+ end
69
+ end
70
+
71
+ sink_before :chown do |_owner_int, group_int, *file_names|
72
+ file_names.each do |file_name|
73
+ Helpers.scan(file_name, "chown")
74
+ end
75
+ end
76
+
77
+ sink_before :utime do |_atime, _mtime, *file_names|
78
+ file_names.each do |file_name|
79
+ Helpers.scan(file_name, "utime")
80
+ end
81
+ end
82
+
83
+ sink_after :join do |result|
84
+ Helpers.scan(result, "join")
85
+ end
86
+
87
+ sink_before :expand_path do |file_name|
88
+ Helpers.scan(file_name, "expand_path")
89
+ end
90
+
91
+ sink_before :realpath do |file_name|
92
+ Helpers.scan(file_name, "realpath")
93
+ end
94
+
95
+ sink_before :realdirpath do |file_name|
96
+ Helpers.scan(file_name, "realdirpath")
97
+ end
98
+ end
99
+
100
+ ::File.class_eval do
101
+ extend Sinks::DSL
102
+
103
+ sink_before :initialize do |path|
104
+ Helpers.scan(path, "new")
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ 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!