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