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.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +674 -0
- data/README.md +40 -0
- data/Rakefile +63 -0
- data/lib/aikido/zen/actor.rb +116 -0
- data/lib/aikido/zen/agent.rb +187 -0
- data/lib/aikido/zen/api_client.rb +132 -0
- data/lib/aikido/zen/attack.rb +138 -0
- data/lib/aikido/zen/capped_collections.rb +68 -0
- data/lib/aikido/zen/config.rb +229 -0
- data/lib/aikido/zen/context/rack_request.rb +24 -0
- data/lib/aikido/zen/context/rails_request.rb +42 -0
- data/lib/aikido/zen/context.rb +101 -0
- data/lib/aikido/zen/errors.rb +88 -0
- data/lib/aikido/zen/event.rb +66 -0
- data/lib/aikido/zen/internals.rb +64 -0
- data/lib/aikido/zen/libzen-v0.1.26.aarch64.dylib +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
- data/lib/aikido/zen/middleware/set_context.rb +26 -0
- data/lib/aikido/zen/middleware/throttler.rb +50 -0
- data/lib/aikido/zen/outbound_connection.rb +45 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +48 -0
- data/lib/aikido/zen/rails_engine.rb +53 -0
- data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
- data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
- data/lib/aikido/zen/rate_limiter/result.rb +31 -0
- data/lib/aikido/zen/rate_limiter.rb +55 -0
- data/lib/aikido/zen/request/heuristic_router.rb +109 -0
- data/lib/aikido/zen/request/rails_router.rb +84 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
- data/lib/aikido/zen/request/schema/builder.rb +125 -0
- data/lib/aikido/zen/request/schema/definition.rb +112 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +72 -0
- data/lib/aikido/zen/request.rb +97 -0
- data/lib/aikido/zen/route.rb +39 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
- data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
- data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
- data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
- data/lib/aikido/zen/runtime_settings.rb +70 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
- data/lib/aikido/zen/scanners.rb +5 -0
- data/lib/aikido/zen/sink.rb +108 -0
- data/lib/aikido/zen/sinks/async_http.rb +63 -0
- data/lib/aikido/zen/sinks/curb.rb +89 -0
- data/lib/aikido/zen/sinks/em_http.rb +71 -0
- data/lib/aikido/zen/sinks/excon.rb +103 -0
- data/lib/aikido/zen/sinks/http.rb +76 -0
- data/lib/aikido/zen/sinks/httpclient.rb +68 -0
- data/lib/aikido/zen/sinks/httpx.rb +61 -0
- data/lib/aikido/zen/sinks/mysql2.rb +21 -0
- data/lib/aikido/zen/sinks/net_http.rb +85 -0
- data/lib/aikido/zen/sinks/patron.rb +88 -0
- data/lib/aikido/zen/sinks/pg.rb +50 -0
- data/lib/aikido/zen/sinks/resolv.rb +41 -0
- data/lib/aikido/zen/sinks/socket.rb +51 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
- data/lib/aikido/zen/sinks/trilogy.rb +21 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +21 -0
- data/lib/aikido/zen/stats/routes.rb +53 -0
- data/lib/aikido/zen/stats/sink_stats.rb +95 -0
- data/lib/aikido/zen/stats/users.rb +26 -0
- data/lib/aikido/zen/stats.rb +171 -0
- data/lib/aikido/zen/synchronizable.rb +24 -0
- data/lib/aikido/zen/system_info.rb +84 -0
- data/lib/aikido/zen/version.rb +10 -0
- data/lib/aikido/zen.rb +138 -0
- data/lib/aikido-zen.rb +3 -0
- data/lib/aikido.rb +3 -0
- data/tasklib/libzen.rake +128 -0
- 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)
|