aikido-zen 1.0.1.beta.5-arm64-linux → 1.0.2-arm64-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 +4 -4
- data/.simplecov +6 -0
- data/README.md +2 -0
- data/benchmarks/README.md +0 -1
- data/benchmarks/rails7.1_benchmark.js +1 -0
- data/benchmarks/rails7.1_sql_injection.js +52 -20
- data/docs/config.md +9 -1
- data/docs/proxy.md +10 -0
- data/docs/rails.md +55 -13
- data/docs/troubleshooting.md +62 -0
- data/lib/aikido/zen/actor.rb +34 -4
- data/lib/aikido/zen/agent/heartbeats_manager.rb +5 -5
- data/lib/aikido/zen/agent.rb +19 -17
- data/lib/aikido/zen/attack.rb +19 -9
- data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
- data/lib/aikido/zen/attack_wave.rb +88 -0
- data/lib/aikido/zen/cache.rb +91 -0
- data/lib/aikido/zen/capped_collections.rb +22 -4
- data/lib/aikido/zen/collector/event.rb +238 -0
- data/lib/aikido/zen/collector/hosts.rb +16 -1
- data/lib/aikido/zen/collector/routes.rb +13 -8
- data/lib/aikido/zen/collector/stats.rb +33 -22
- data/lib/aikido/zen/collector/users.rb +5 -3
- data/lib/aikido/zen/collector.rb +107 -28
- data/lib/aikido/zen/config.rb +54 -21
- data/lib/aikido/zen/context/rack_request.rb +3 -0
- data/lib/aikido/zen/context/rails_request.rb +3 -0
- data/lib/aikido/zen/context.rb +42 -9
- data/lib/aikido/zen/detached_agent/agent.rb +28 -27
- data/lib/aikido/zen/detached_agent/front_object.rb +10 -6
- data/lib/aikido/zen/detached_agent/server.rb +63 -26
- data/lib/aikido/zen/event.rb +47 -2
- data/lib/aikido/zen/helpers.rb +24 -0
- data/lib/aikido/zen/internals.rb +23 -3
- data/lib/aikido/zen/libzen-v0.1.48-arm64-linux.so +0 -0
- data/lib/aikido/zen/middleware/{check_allowed_addresses.rb → allowed_address_checker.rb} +1 -1
- data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
- data/lib/aikido/zen/middleware/{set_context.rb → context_setter.rb} +1 -1
- data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +3 -1
- data/lib/aikido/zen/middleware/request_tracker.rb +9 -4
- data/lib/aikido/zen/outbound_connection.rb +18 -1
- data/lib/aikido/zen/payload.rb +1 -1
- data/lib/aikido/zen/rails_engine.rb +5 -8
- data/lib/aikido/zen/request/rails_router.rb +17 -2
- data/lib/aikido/zen/request.rb +21 -36
- data/lib/aikido/zen/route.rb +57 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +37 -8
- data/lib/aikido/zen/runtime_settings.rb +6 -5
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +10 -7
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +5 -4
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +3 -2
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +3 -2
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +2 -1
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +8 -2
- data/lib/aikido/zen/sink.rb +1 -1
- data/lib/aikido/zen/sinks/action_controller.rb +3 -1
- data/lib/aikido/zen/sinks/file.rb +45 -4
- data/lib/aikido/zen/sinks/kernel.rb +1 -1
- data/lib/aikido/zen/sinks/socket.rb +7 -0
- data/lib/aikido/zen/sinks_dsl.rb +12 -0
- data/lib/aikido/zen/system_info.rb +1 -5
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen.rb +77 -15
- data/tasklib/bench.rake +1 -1
- data/tasklib/libzen.rake +1 -0
- metadata +15 -5
- data/lib/aikido/zen/libzen-v0.1.39-arm64-linux.so +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido
|
|
4
|
+
module Zen
|
|
5
|
+
module Middleware
|
|
6
|
+
# This middleware is responsible for detecting when a process has forked
|
|
7
|
+
# (e.g., in a Puma or Unicorn worker) and resetting the state of the
|
|
8
|
+
# Aikido Zen agent. It should be inserted early in the middleware stack.
|
|
9
|
+
class ForkDetector
|
|
10
|
+
def initialize(app)
|
|
11
|
+
@app = app
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
# This is the single, reliable trigger point for the fork check.
|
|
16
|
+
Aikido::Zen.check_and_handle_fork
|
|
17
|
+
|
|
18
|
+
@app.call(env)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -33,8 +33,10 @@ module Aikido::Zen
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
35
|
def should_throttle?(request)
|
|
36
|
+
# Bypass rate limiting for allowed IPs
|
|
37
|
+
return false if @settings.allowed_ips.include?(request.ip)
|
|
38
|
+
|
|
36
39
|
return false unless @settings.endpoints[request.route].rate_limiting.enabled?
|
|
37
|
-
return false if @settings.skip_protection_for_ips.include?(request.ip)
|
|
38
40
|
|
|
39
41
|
result = @detached_agent.calculate_rate_limits(request)
|
|
40
42
|
|
|
@@ -5,8 +5,9 @@ module Aikido::Zen
|
|
|
5
5
|
# Rack middleware used to track request
|
|
6
6
|
# It implements the logic under that which is considered worthy of being tracked.
|
|
7
7
|
class RequestTracker
|
|
8
|
-
def initialize(app)
|
|
8
|
+
def initialize(app, settings: Aikido::Zen.runtime_settings)
|
|
9
9
|
@app = app
|
|
10
|
+
@settings = settings
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def call(env)
|
|
@@ -16,9 +17,10 @@ module Aikido::Zen
|
|
|
16
17
|
if request.route && track?(
|
|
17
18
|
status_code: response[0],
|
|
18
19
|
route: request.route.path,
|
|
19
|
-
http_method: request.request_method
|
|
20
|
+
http_method: request.request_method,
|
|
21
|
+
ip: request.ip
|
|
20
22
|
)
|
|
21
|
-
Aikido::Zen.track_request
|
|
23
|
+
Aikido::Zen.track_request(request)
|
|
22
24
|
|
|
23
25
|
if Aikido::Zen.config.collect_api_schema?
|
|
24
26
|
Aikido::Zen.track_discovered_route(request)
|
|
@@ -126,7 +128,10 @@ module Aikido::Zen
|
|
|
126
128
|
# @param status_code [Integer]
|
|
127
129
|
# @param route [String]
|
|
128
130
|
# @param http_method [String]
|
|
129
|
-
def track?(status_code:, route:, http_method:)
|
|
131
|
+
def track?(status_code:, route:, http_method:, ip: nil)
|
|
132
|
+
# Bypass request and route tracking for allowed IPs
|
|
133
|
+
return false if @settings.allowed_ips.include?(ip)
|
|
134
|
+
|
|
130
135
|
# In the UI we want to show only successful (2xx) or redirect (3xx) responses
|
|
131
136
|
# anything else is discarded.
|
|
132
137
|
return false unless status_code >= 200 && status_code <= 399
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
module Aikido::Zen
|
|
4
4
|
# Simple data object to identify connections performed to outbound servers.
|
|
5
5
|
class OutboundConnection
|
|
6
|
+
def self.from_json(data)
|
|
7
|
+
new(
|
|
8
|
+
host: data[:hostname],
|
|
9
|
+
port: data[:port]
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
6
13
|
# Convenience factory to create connection descriptions out of URI objects.
|
|
7
14
|
#
|
|
8
15
|
# @param uri [URI]
|
|
@@ -18,13 +25,23 @@ module Aikido::Zen
|
|
|
18
25
|
# @return [Integer] the port number to which the connection was attempted.
|
|
19
26
|
attr_reader :port
|
|
20
27
|
|
|
28
|
+
# @return [Integer] the number of times that this connection was seen by
|
|
29
|
+
# the hosts collector.
|
|
30
|
+
attr_reader :hits
|
|
31
|
+
|
|
21
32
|
def initialize(host:, port:)
|
|
22
33
|
@host = host
|
|
23
34
|
@port = port
|
|
24
35
|
end
|
|
25
36
|
|
|
37
|
+
def hit
|
|
38
|
+
# Lazy initialize @hits, so it stays nil until the connection is tracked.
|
|
39
|
+
@hits ||= 0
|
|
40
|
+
@hits += 1
|
|
41
|
+
end
|
|
42
|
+
|
|
26
43
|
def as_json
|
|
27
|
-
{hostname: host, port: port}
|
|
44
|
+
{hostname: host, port: port, hits: hits}.compact
|
|
28
45
|
end
|
|
29
46
|
|
|
30
47
|
def ==(other)
|
data/lib/aikido/zen/payload.rb
CHANGED
|
@@ -10,8 +10,11 @@ module Aikido::Zen
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
initializer "aikido.add_middleware" do |app|
|
|
13
|
-
app.middleware.
|
|
14
|
-
|
|
13
|
+
app.middleware.insert_before 0, Aikido::Zen::Middleware::ForkDetector
|
|
14
|
+
|
|
15
|
+
app.middleware.use Aikido::Zen::Middleware::ContextSetter
|
|
16
|
+
app.middleware.use Aikido::Zen::Middleware::AllowedAddressChecker
|
|
17
|
+
app.middleware.use Aikido::Zen::Middleware::AttackWaveProtector
|
|
15
18
|
# Request Tracker stats do not consider failed request or 40x, so the middleware
|
|
16
19
|
# must be the last one wrapping the request.
|
|
17
20
|
app.middleware.use Aikido::Zen::Middleware::RequestTracker
|
|
@@ -29,12 +32,6 @@ module Aikido::Zen
|
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
initializer "aikido.configuration" do |app|
|
|
32
|
-
# Allow the logger to be configured before checking if disabled? so we can
|
|
33
|
-
# let the user know that the agent is disabled.
|
|
34
|
-
logger = ::Rails.logger
|
|
35
|
-
logger = logger.tagged("aikido") if logger.respond_to?(:tagged)
|
|
36
|
-
app.config.zen.logger = logger
|
|
37
|
-
|
|
38
35
|
app.config.zen.request_builder = Aikido::Zen::Context::RAILS_REQUEST_BUILDER
|
|
39
36
|
|
|
40
37
|
# Plug Rails' JSON encoder/decoder, but only if the user hasn't changed
|
|
@@ -62,16 +62,31 @@ module Aikido::Zen
|
|
|
62
62
|
nil
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
private
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def build_route(route, request, prefix: request.script_name)
|
|
66
68
|
route_wrapper = ActionDispatch::Routing::RouteWrapper.new(route)
|
|
67
69
|
|
|
68
70
|
path = if prefix.present?
|
|
69
|
-
|
|
71
|
+
prefix_route_path(prefix.to_s, route_wrapper.path)
|
|
70
72
|
else
|
|
71
73
|
route_wrapper.path
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
Aikido::Zen::Route.new(verb: request.request_method, path: path)
|
|
75
77
|
end
|
|
78
|
+
|
|
79
|
+
def prefix_route_path(string1, string2)
|
|
80
|
+
# The strings appear to start with "/", allowing them to be concatenated
|
|
81
|
+
# directly after removing trailing "/". However, as it is not currently
|
|
82
|
+
# known whether this is guaranteed, we insert a separator when necessary.
|
|
83
|
+
|
|
84
|
+
separator = string2.start_with?("/") ? "" : "/"
|
|
85
|
+
|
|
86
|
+
string1 = string1.chomp("/")
|
|
87
|
+
string2 = string2.chomp("/")
|
|
88
|
+
|
|
89
|
+
"#{string1}#{separator}#{string2}"
|
|
90
|
+
end
|
|
76
91
|
end
|
|
77
92
|
end
|
data/lib/aikido/zen/request.rb
CHANGED
|
@@ -17,17 +17,16 @@ module Aikido::Zen
|
|
|
17
17
|
# @see Aikido::Zen.track_user
|
|
18
18
|
attr_accessor :actor
|
|
19
19
|
|
|
20
|
-
def initialize(delegate, framework:, router:)
|
|
20
|
+
def initialize(delegate, config = Aikido::Zen.config, framework:, router:)
|
|
21
21
|
super(delegate)
|
|
22
|
+
@config = config
|
|
22
23
|
@framework = framework
|
|
23
24
|
@router = router
|
|
24
|
-
@body_read = false
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def __setobj__(delegate) # :nodoc:
|
|
28
28
|
super
|
|
29
|
-
@
|
|
30
|
-
@route = @normalized_header = @truncated_body = nil
|
|
29
|
+
@route = @normalized_header = nil
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
# @return [Aikido::Zen::Route] the framework route being requested.
|
|
@@ -40,6 +39,22 @@ module Aikido::Zen
|
|
|
40
39
|
@schema ||= Aikido::Zen::Request::Schema.build
|
|
41
40
|
end
|
|
42
41
|
|
|
42
|
+
# @return [String] the IP address of the client making the request.
|
|
43
|
+
def client_ip
|
|
44
|
+
return @client_ip if @client_ip
|
|
45
|
+
|
|
46
|
+
if @config.client_ip_header
|
|
47
|
+
value = env[@config.client_ip_header]
|
|
48
|
+
if Resolv::AddressRegex.match?(value)
|
|
49
|
+
@client_ip = value
|
|
50
|
+
else
|
|
51
|
+
@config.logger.warn("Invalid IP address in custom client IP header `#{@config.client_ip_header}`: `#{value}`")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@client_ip ||= respond_to?(:remote_ip) ? remote_ip : ip
|
|
56
|
+
end
|
|
57
|
+
|
|
43
58
|
# Map the CGI-style env Hash into "pretty-looking" headers, preserving the
|
|
44
59
|
# values as-is. For example, HTTP_ACCEPT turns into "Accept", CONTENT_TYPE
|
|
45
60
|
# turns into "Content-Type", and HTTP_X_FORWARDED_FOR turns into
|
|
@@ -55,42 +70,12 @@ module Aikido::Zen
|
|
|
55
70
|
}
|
|
56
71
|
end
|
|
57
72
|
|
|
58
|
-
# @api private
|
|
59
|
-
#
|
|
60
|
-
# Reads the first 16KiB of the request body, to include in attack reports
|
|
61
|
-
# back to the Aikido server. This method should only be called if an attack
|
|
62
|
-
# is detected during the current request.
|
|
63
|
-
#
|
|
64
|
-
# If the underlying IO object has been partially (or fully) read before,
|
|
65
|
-
# this will attempt to restore the previous cursor position after reading it
|
|
66
|
-
# if possible, or leave if rewund if not.
|
|
67
|
-
#
|
|
68
|
-
# @param max_size [Integer] number of bytes to read at most.
|
|
69
|
-
#
|
|
70
|
-
# @return [String]
|
|
71
|
-
def truncated_body(max_size: 16384)
|
|
72
|
-
return @truncated_body if @body_read
|
|
73
|
-
return nil if body.nil?
|
|
74
|
-
|
|
75
|
-
begin
|
|
76
|
-
initial_pos = body.pos if body.respond_to?(:pos)
|
|
77
|
-
body.rewind
|
|
78
|
-
@truncated_body = body.read(max_size)
|
|
79
|
-
ensure
|
|
80
|
-
@body_read = true
|
|
81
|
-
body.rewind
|
|
82
|
-
body.seek(initial_pos) if initial_pos && body.respond_to?(:seek)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
73
|
def as_json
|
|
87
74
|
{
|
|
88
|
-
method: request_method.
|
|
75
|
+
method: request_method.upcase,
|
|
89
76
|
url: url,
|
|
90
|
-
ipAddress:
|
|
77
|
+
ipAddress: client_ip,
|
|
91
78
|
userAgent: user_agent,
|
|
92
|
-
headers: normalized_headers.reject { |_, val| val.to_s.empty? },
|
|
93
|
-
body: truncated_body,
|
|
94
79
|
source: framework,
|
|
95
80
|
route: route&.path
|
|
96
81
|
}
|
data/lib/aikido/zen/route.rb
CHANGED
|
@@ -5,6 +5,13 @@ module Aikido::Zen
|
|
|
5
5
|
# framework to go from a given HTTP request to the code that handles said
|
|
6
6
|
# request.
|
|
7
7
|
class Route
|
|
8
|
+
def self.from_json(data)
|
|
9
|
+
new(
|
|
10
|
+
verb: data[:method],
|
|
11
|
+
path: data[:path]
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
8
15
|
# @return [String] the HTTP verb used to request this route.
|
|
9
16
|
attr_reader :verb
|
|
10
17
|
|
|
@@ -32,8 +39,58 @@ module Aikido::Zen
|
|
|
32
39
|
[verb, path].hash
|
|
33
40
|
end
|
|
34
41
|
|
|
42
|
+
# Sort routes by wildcard matching order deterministically:
|
|
43
|
+
#
|
|
44
|
+
# 1. Exact path before wildcard path
|
|
45
|
+
# 2. Fewer wildcards in path relative to path length
|
|
46
|
+
# 3. Earliest wildcard position in path
|
|
47
|
+
# 4. Exact verb before wildcard verb
|
|
48
|
+
# 5. Lexicographic path (tie-break)
|
|
49
|
+
# 6. Lexicographic verb (tie-break)
|
|
50
|
+
#
|
|
51
|
+
# @return [Array] the sort key
|
|
52
|
+
def sort_key
|
|
53
|
+
@sort_key ||= begin
|
|
54
|
+
stars = []
|
|
55
|
+
i = -1
|
|
56
|
+
while (i = path.index("*", i + 1))
|
|
57
|
+
stars << i
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
[
|
|
61
|
+
stars.empty? ? 0 : 1,
|
|
62
|
+
stars.length - path.length,
|
|
63
|
+
stars,
|
|
64
|
+
(verb == "*") ? 1 : 0,
|
|
65
|
+
path,
|
|
66
|
+
verb
|
|
67
|
+
].freeze
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def match?(other)
|
|
72
|
+
other.is_a?(Route) &&
|
|
73
|
+
pattern(verb).match?(other.verb) &&
|
|
74
|
+
pattern(path).match?(other.path)
|
|
75
|
+
end
|
|
76
|
+
|
|
35
77
|
def inspect
|
|
36
78
|
"#<#{self.class.name} #{verb} #{path.inspect}>"
|
|
37
79
|
end
|
|
80
|
+
|
|
81
|
+
# Construct a regular expression equivalent to the wildcard string,
|
|
82
|
+
# where '*' is the wildcard operator.
|
|
83
|
+
#
|
|
84
|
+
# The resulting pattern matches the entire input, allows an optional
|
|
85
|
+
# trailing slash, and is case-insensitive.
|
|
86
|
+
#
|
|
87
|
+
# All other special characters in the regular expression are escaped
|
|
88
|
+
# so that they are treated literally.
|
|
89
|
+
#
|
|
90
|
+
# @param string [String] wildcard string
|
|
91
|
+
# @return [Regexp] regular expression matching the wildcard string
|
|
92
|
+
private def pattern(string)
|
|
93
|
+
/^#{Regexp.escape(string).gsub("\\*", ".*")}\/?$/i
|
|
94
|
+
end
|
|
38
95
|
end
|
|
39
96
|
end
|
|
@@ -16,24 +16,53 @@ module Aikido::Zen
|
|
|
16
16
|
# @param data [Array<Hash>]
|
|
17
17
|
# @return [Aikido::Zen::RuntimeSettings::Endpoints]
|
|
18
18
|
def self.from_json(data)
|
|
19
|
-
|
|
20
|
-
route = Route.new(verb:
|
|
21
|
-
settings = RuntimeSettings::ProtectionSettings.from_json(
|
|
19
|
+
endpoint_pairs = Array(data).map do |value|
|
|
20
|
+
route = Route.new(verb: value["method"], path: value["route"])
|
|
21
|
+
settings = RuntimeSettings::ProtectionSettings.from_json(value)
|
|
22
22
|
[route, settings]
|
|
23
|
-
|
|
23
|
+
end
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# Sort endpoints by wildcard matching order
|
|
26
|
+
endpoint_pairs.sort_by! do |route, settings|
|
|
27
|
+
route.sort_key
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
new(endpoint_pairs.to_h)
|
|
26
31
|
end
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
# @param endpoints [Hash] the endpoints in wildcard matching order
|
|
34
|
+
# @return [Aikido::Zen::RuntimeSettings::Endpoints]
|
|
35
|
+
def initialize(endpoints = {})
|
|
36
|
+
@endpoints = endpoints
|
|
30
37
|
@endpoints.default = RuntimeSettings::ProtectionSettings.none
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
# @param route [Aikido::Zen::Route]
|
|
34
41
|
# @return [Aikido::Zen::RuntimeSettings::ProtectionSettings]
|
|
35
42
|
def [](route)
|
|
36
|
-
@endpoints[route]
|
|
43
|
+
return @endpoints[route] if @endpoints.key?(route)
|
|
44
|
+
|
|
45
|
+
# Wildcard endpoint matching
|
|
46
|
+
|
|
47
|
+
@endpoints.each do |pattern, settings|
|
|
48
|
+
return settings if pattern.match?(route)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@endpoints.default
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param route [Aikido::Zen::Route]
|
|
55
|
+
# @return [Array<Aikido::Zen::RuntimeSettings::ProtectionSettings>]
|
|
56
|
+
def match(route)
|
|
57
|
+
matches = []
|
|
58
|
+
|
|
59
|
+
@endpoints.each do |pattern, settings|
|
|
60
|
+
matches << settings if pattern.match?(route)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
matches << @endpoints.default if matches.empty?
|
|
64
|
+
|
|
65
|
+
matches
|
|
37
66
|
end
|
|
38
67
|
|
|
39
68
|
# @!visibility private
|
|
@@ -11,11 +11,11 @@ module Aikido::Zen
|
|
|
11
11
|
#
|
|
12
12
|
# You can subscribe to changes with +#add_observer(object, func_name)+, which
|
|
13
13
|
# will call the function passing the settings as an argument.
|
|
14
|
-
RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :
|
|
14
|
+
RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :allowed_ips, :received_any_stats, :blocking_mode) do
|
|
15
15
|
def initialize(*)
|
|
16
16
|
super
|
|
17
17
|
self.endpoints ||= RuntimeSettings::Endpoints.new
|
|
18
|
-
self.
|
|
18
|
+
self.allowed_ips ||= RuntimeSettings::IPSet.new
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# @!attribute [rw] updated_at
|
|
@@ -35,7 +35,7 @@ module Aikido::Zen
|
|
|
35
35
|
# @!attribute [rw] blocked_user_ids
|
|
36
36
|
# @return [Array]
|
|
37
37
|
|
|
38
|
-
# @!attribute [rw]
|
|
38
|
+
# @!attribute [rw] allowed_ips
|
|
39
39
|
# @return [Aikido::Zen::RuntimeSettings::IPSet]
|
|
40
40
|
|
|
41
41
|
# Parse and interpret the JSON response from the core API with updated
|
|
@@ -50,11 +50,12 @@ module Aikido::Zen
|
|
|
50
50
|
last_updated_at = updated_at
|
|
51
51
|
|
|
52
52
|
self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
|
|
53
|
-
self.heartbeat_interval =
|
|
53
|
+
self.heartbeat_interval = data["heartbeatIntervalInMS"].to_i / 1000
|
|
54
54
|
self.endpoints = RuntimeSettings::Endpoints.from_json(data["endpoints"])
|
|
55
55
|
self.blocked_user_ids = data["blockedUserIds"]
|
|
56
|
-
self.
|
|
56
|
+
self.allowed_ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
|
|
57
57
|
self.received_any_stats = data["receivedAnyStats"]
|
|
58
|
+
self.blocking_mode = data["block"]
|
|
58
59
|
|
|
59
60
|
updated_at != last_updated_at
|
|
60
61
|
end
|
|
@@ -4,7 +4,8 @@ module Aikido::Zen
|
|
|
4
4
|
module Scanners
|
|
5
5
|
module PathTraversal
|
|
6
6
|
DANGEROUS_PATH_PARTS = ["../", "..\\"]
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
LINUX_PATH_STARTS = [
|
|
8
9
|
"/bin/",
|
|
9
10
|
"/boot/",
|
|
10
11
|
"/dev/",
|
|
@@ -26,10 +27,12 @@ module Aikido::Zen
|
|
|
26
27
|
"/var/"
|
|
27
28
|
]
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
WINDOWS_PATH_STARTS = ["c:/", "c:\\"]
|
|
31
|
+
|
|
32
|
+
DANGEROUS_PATH_STARTS = LINUX_PATH_STARTS + WINDOWS_PATH_STARTS
|
|
30
33
|
|
|
31
34
|
module Helpers
|
|
32
|
-
def self.
|
|
35
|
+
def self.include_unsafe_path_parts?(filepath)
|
|
33
36
|
DANGEROUS_PATH_PARTS.each do |dangerous_part|
|
|
34
37
|
return true if filepath.include?(dangerous_part)
|
|
35
38
|
end
|
|
@@ -37,7 +40,7 @@ module Aikido::Zen
|
|
|
37
40
|
false
|
|
38
41
|
end
|
|
39
42
|
|
|
40
|
-
def self.
|
|
43
|
+
def self.start_with_unsafe_path?(filepath, user_input)
|
|
41
44
|
# Check if path is relative (not absolute or drive letter path)
|
|
42
45
|
# Required because `expand_path` will build absolute paths from relative paths
|
|
43
46
|
return false if Pathname.new(filepath).relative? || Pathname.new(user_input).relative?
|
|
@@ -51,12 +54,12 @@ module Aikido::Zen
|
|
|
51
54
|
# to prevent false positives.
|
|
52
55
|
# e.g., if user input is /etc/ and the path is /etc/passwd, we don't want to flag it,
|
|
53
56
|
# as long as the user input does not contain a subdirectory or filename
|
|
54
|
-
if user_input == dangerous_start || user_input == dangerous_start.chomp("/")
|
|
55
|
-
|
|
56
|
-
end
|
|
57
|
+
return false if user_input == dangerous_start || user_input == dangerous_start.chomp("/")
|
|
58
|
+
|
|
57
59
|
return true
|
|
58
60
|
end
|
|
59
61
|
end
|
|
62
|
+
|
|
60
63
|
false
|
|
61
64
|
end
|
|
62
65
|
end
|
|
@@ -21,14 +21,15 @@ module Aikido::Zen
|
|
|
21
21
|
# user input is detected to be attempting a Path Traversal Attack, or +nil+ if not.
|
|
22
22
|
def self.call(filepath:, sink:, context:, operation:)
|
|
23
23
|
context.payloads.each do |payload|
|
|
24
|
-
next unless new(filepath, payload.value).attack?
|
|
24
|
+
next unless new(filepath, payload.value.to_s).attack?
|
|
25
25
|
|
|
26
26
|
return Attacks::PathTraversalAttack.new(
|
|
27
27
|
sink: sink,
|
|
28
28
|
input: payload,
|
|
29
29
|
filepath: filepath,
|
|
30
30
|
context: context,
|
|
31
|
-
operation: "#{sink.operation}.#{operation}"
|
|
31
|
+
operation: "#{sink.operation}.#{operation}",
|
|
32
|
+
stack: Aikido::Zen.clean_stack_trace
|
|
32
33
|
)
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -51,12 +52,12 @@ module Aikido::Zen
|
|
|
51
52
|
# We ignore cases where the user input is not part of the file path.
|
|
52
53
|
return false unless @filepath.include?(@input)
|
|
53
54
|
|
|
54
|
-
if PathTraversal::Helpers.
|
|
55
|
+
if PathTraversal::Helpers.include_unsafe_path_parts?(@filepath) && PathTraversal::Helpers.include_unsafe_path_parts?(@input)
|
|
55
56
|
return true
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
# Check for absolute path traversal
|
|
59
|
-
PathTraversal::Helpers.
|
|
60
|
+
PathTraversal::Helpers.start_with_unsafe_path?(@filepath, @input)
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
end
|
|
@@ -16,14 +16,15 @@ module Aikido::Zen
|
|
|
16
16
|
#
|
|
17
17
|
def self.call(command:, sink:, context:, operation:)
|
|
18
18
|
context.payloads.each do |payload|
|
|
19
|
-
next unless new(command, payload.value).attack?
|
|
19
|
+
next unless new(command, payload.value.to_s).attack?
|
|
20
20
|
|
|
21
21
|
return Attacks::ShellInjectionAttack.new(
|
|
22
22
|
sink: sink,
|
|
23
23
|
input: payload,
|
|
24
24
|
command: command,
|
|
25
25
|
context: context,
|
|
26
|
-
operation: "#{sink.operation}.#{operation}"
|
|
26
|
+
operation: "#{sink.operation}.#{operation}",
|
|
27
|
+
stack: Aikido::Zen.clean_stack_trace
|
|
27
28
|
)
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -32,7 +32,7 @@ module Aikido::Zen
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
context.payloads.each do |payload|
|
|
35
|
-
next unless new(query, payload.value, dialect).attack?
|
|
35
|
+
next unless new(query, payload.value.to_s, dialect).attack?
|
|
36
36
|
|
|
37
37
|
return Attacks::SQLInjectionAttack.new(
|
|
38
38
|
sink: sink,
|
|
@@ -40,7 +40,8 @@ module Aikido::Zen
|
|
|
40
40
|
input: payload,
|
|
41
41
|
dialect: dialect,
|
|
42
42
|
context: context,
|
|
43
|
-
operation: "#{sink.operation}.#{operation}"
|
|
43
|
+
operation: "#{sink.operation}.#{operation}",
|
|
44
|
+
stack: Aikido::Zen.clean_stack_trace
|
|
44
45
|
)
|
|
45
46
|
end
|
|
46
47
|
|
|
@@ -20,7 +20,8 @@ module Aikido::Zen
|
|
|
20
20
|
address: offending_address,
|
|
21
21
|
sink: sink,
|
|
22
22
|
context: context,
|
|
23
|
-
operation: "#{sink.operation}.#{operation}"
|
|
23
|
+
operation: "#{sink.operation}.#{operation}",
|
|
24
|
+
stack: Aikido::Zen.clean_stack_trace
|
|
24
25
|
)
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -33,7 +34,9 @@ module Aikido::Zen
|
|
|
33
34
|
# @return [String, nil] either the offending address, or +nil+ if no
|
|
34
35
|
# address is deemed dangerous.
|
|
35
36
|
def attack?
|
|
36
|
-
return
|
|
37
|
+
return unless @config.stored_ssrf? # Feature flag
|
|
38
|
+
|
|
39
|
+
return if @config.imds_allowed_hosts.include?(@hostname)
|
|
37
40
|
|
|
38
41
|
@addresses.find do |candidate|
|
|
39
42
|
DANGEROUS_ADDRESSES.any? { |address| address === candidate }
|
|
@@ -42,6 +45,9 @@ module Aikido::Zen
|
|
|
42
45
|
|
|
43
46
|
DANGEROUS_ADDRESSES = [
|
|
44
47
|
IPAddr.new("169.254.169.254"),
|
|
48
|
+
IPAddr.new("100.100.100.200"),
|
|
49
|
+
IPAddr.new("::ffff:169.254.169.254"),
|
|
50
|
+
IPAddr.new("::ffff:100.100.100.200"),
|
|
45
51
|
IPAddr.new("fd00:ec2::254")
|
|
46
52
|
]
|
|
47
53
|
end
|
data/lib/aikido/zen/sink.rb
CHANGED
|
@@ -43,8 +43,10 @@ module Aikido::Zen
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
private def should_throttle?(request)
|
|
46
|
+
# Bypass rate limiting for allowed IPs
|
|
47
|
+
return false if @settings.allowed_ips.include?(request.ip)
|
|
48
|
+
|
|
46
49
|
return false unless @settings.endpoints[request.route].rate_limiting.enabled?
|
|
47
|
-
return false if @settings.skip_protection_for_ips.include?(request.ip)
|
|
48
50
|
|
|
49
51
|
result = @detached_agent.calculate_rate_limits(request)
|
|
50
52
|
return false unless result
|