aikido-zen 1.9.0.beta.3-arm64-linux → 1.9.0.beta.4-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/docs/rails.md +14 -0
- data/lib/aikido/zen/agent.rb +12 -5
- data/lib/aikido/zen/attack.rb +9 -3
- data/lib/aikido/zen/config.rb +17 -1
- data/lib/aikido/zen/context.rb +32 -10
- data/lib/aikido/zen/helpers.rb +16 -0
- data/lib/aikido/zen/internals.rb +3 -11
- data/lib/aikido/zen/payload.rb +5 -2
- data/lib/aikido/zen/request.rb +12 -3
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +2 -2
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +2 -2
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +2 -2
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +8 -3
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +14 -1
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen/worker_process/agent/client.rb +4 -1
- data/lib/aikido/zen/worker_process/agent/server.rb +5 -5
- data/lib/aikido/zen.rb +38 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ada5e55768a0adb9c9311bf01868950a02636af957a14da9766e62a0e0fcb6a6
|
|
4
|
+
data.tar.gz: 8ace3f901aec31ba41afb86cac79dec53be98a60ec55226b65351aa417b73313
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da321436b2e4cdb30c8fa6d3456c771bb9bb56f44afd9b5269a5f5cef1f6af8bdb59ab4dca7a05ce2336d46ac98b57aa407307514270cb7ec7f55707d2fd857c
|
|
7
|
+
data.tar.gz: 9ae53b66ccd877aae99d0fd59735c23e5e6bc9ffcc71231835eb52e0f4cfce4f51ebb594458df56dcbb9ec855646c46178e59f26040bd7a4a1d870056fc957cb
|
data/docs/rails.md
CHANGED
|
@@ -75,6 +75,20 @@ class ApplicationController < ActionController::Base
|
|
|
75
75
|
end
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
### Rate limiting groups
|
|
79
|
+
|
|
80
|
+
To limit the number of requests for a group of users, you can use the
|
|
81
|
+
`Aikido::Zen.set_rate_limit_group` method. For example, this is useful if you
|
|
82
|
+
want to limit the number of requests per team or company.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
Aikido::Zen.set_rate_limit_group(current_user.team_id)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> [!NOTE]
|
|
89
|
+
> If a rate limiting group is set, the configured rate limits are only applied
|
|
90
|
+
> to the group and not to individual users or IP addresses.
|
|
91
|
+
|
|
78
92
|
## Configuration
|
|
79
93
|
|
|
80
94
|
Zen exposes its configuration object to the Rails configuration, which you can
|
data/lib/aikido/zen/agent.rb
CHANGED
|
@@ -134,12 +134,19 @@ module Aikido::Zen
|
|
|
134
134
|
def handle_attack(attack)
|
|
135
135
|
attack.will_be_blocked! if Aikido::Zen.blocking_mode?
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
# Recording the attack must never prevent us from blocking it.
|
|
138
|
+
begin
|
|
139
|
+
@config.logger.error(
|
|
140
|
+
format("Zen has %s a %s: %s", attack.blocked? ? "blocked" : "detected", attack.humanized_name, attack.as_json.to_json)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
|
|
144
|
+
|
|
145
|
+
@collector.track_attack(attack)
|
|
146
|
+
rescue => err
|
|
147
|
+
@config.logger.error("Zen failed to record an attack: #{err.class}: #{err.message}")
|
|
148
|
+
end
|
|
141
149
|
|
|
142
|
-
@collector.track_attack(attack)
|
|
143
150
|
raise attack if attack.blocked?
|
|
144
151
|
end
|
|
145
152
|
|
data/lib/aikido/zen/attack.rb
CHANGED
|
@@ -69,8 +69,10 @@ module Aikido::Zen
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def metadata
|
|
72
|
+
safe_filepath = Aikido::Zen::Helpers.encode_safely(filepath)
|
|
73
|
+
|
|
72
74
|
{
|
|
73
|
-
"filename" =>
|
|
75
|
+
"filename" => safe_filepath
|
|
74
76
|
}
|
|
75
77
|
end
|
|
76
78
|
|
|
@@ -106,8 +108,10 @@ module Aikido::Zen
|
|
|
106
108
|
end
|
|
107
109
|
|
|
108
110
|
def metadata
|
|
111
|
+
safe_command = Aikido::Zen::Helpers.encode_safely(@command)
|
|
112
|
+
|
|
109
113
|
{
|
|
110
|
-
"command" =>
|
|
114
|
+
"command" => safe_command
|
|
111
115
|
}
|
|
112
116
|
end
|
|
113
117
|
|
|
@@ -138,8 +142,10 @@ module Aikido::Zen
|
|
|
138
142
|
end
|
|
139
143
|
|
|
140
144
|
def metadata
|
|
145
|
+
safe_query = Aikido::Zen::Helpers.encode_safely(@query)
|
|
146
|
+
|
|
141
147
|
{
|
|
142
|
-
"sql" =>
|
|
148
|
+
"sql" => safe_query,
|
|
143
149
|
"dialect" => @dialect.name,
|
|
144
150
|
"failedToTokenize" => @failed_to_tokenize || nil
|
|
145
151
|
}.compact
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -56,6 +56,10 @@ module Aikido::Zen
|
|
|
56
56
|
# each initial heartbeat event.
|
|
57
57
|
attr_accessor :initial_heartbeat_delays
|
|
58
58
|
|
|
59
|
+
# @return [Integer] the maximum number of levels deep we want to traverse
|
|
60
|
+
# when extracting payloads from a nested request property. Defaults to 64.
|
|
61
|
+
attr_accessor :extract_payloads_max_depth
|
|
62
|
+
|
|
59
63
|
# @return [Symbol] the agent mode for forked worker processes. Can be set
|
|
60
64
|
# through the AIKIDO_AGENT_MODE environment variable.
|
|
61
65
|
attr_reader :agent_mode
|
|
@@ -107,6 +111,10 @@ module Aikido::Zen
|
|
|
107
111
|
# the oldest seen users.
|
|
108
112
|
attr_accessor :max_users_tracked
|
|
109
113
|
|
|
114
|
+
# @return [Integer] the maximum number of custom events tracked per request.
|
|
115
|
+
# Defaults to 25.
|
|
116
|
+
attr_accessor :max_custom_events
|
|
117
|
+
|
|
110
118
|
# @return [Proc{(Aikido::Zen::Request, Symbol, reason: String=nil) => Array(Integer, Hash, #each)}]
|
|
111
119
|
# Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
|
|
112
120
|
# dashboard.
|
|
@@ -249,6 +257,7 @@ module Aikido::Zen
|
|
|
249
257
|
self.api_timeouts = 10
|
|
250
258
|
self.polling_interval = 60 # 1 min
|
|
251
259
|
self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
|
|
260
|
+
self.extract_payloads_max_depth = 64
|
|
252
261
|
self.agent_mode = ENV.fetch("AIKIDO_AGENT_MODE", "shared")
|
|
253
262
|
self.worker_process_polling_interval = 10
|
|
254
263
|
self.worker_process_polling_jitter = 10
|
|
@@ -261,6 +270,7 @@ module Aikido::Zen
|
|
|
261
270
|
self.max_compressed_stats = 100
|
|
262
271
|
self.max_outbound_connections = 200
|
|
263
272
|
self.max_users_tracked = 1000
|
|
273
|
+
self.max_custom_events = 25
|
|
264
274
|
self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
|
|
265
275
|
self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
|
|
266
276
|
self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
|
|
@@ -442,7 +452,13 @@ module Aikido::Zen
|
|
|
442
452
|
|
|
443
453
|
# @!visibility private
|
|
444
454
|
DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
|
|
445
|
-
|
|
455
|
+
rate_limiting_group = request.rate_limiting_group
|
|
456
|
+
return "group:#{rate_limiting_group}" unless rate_limiting_group.nil?
|
|
457
|
+
|
|
458
|
+
actor = request.actor
|
|
459
|
+
return "actor:#{request.actor.id}" unless actor.nil?
|
|
460
|
+
|
|
461
|
+
request.client_ip
|
|
446
462
|
}
|
|
447
463
|
end
|
|
448
464
|
end
|
data/lib/aikido/zen/context.rb
CHANGED
|
@@ -38,6 +38,9 @@ module Aikido::Zen
|
|
|
38
38
|
attr_accessor :idor_protection_enabled
|
|
39
39
|
alias_method :idor_protection_enabled?, :idor_protection_enabled
|
|
40
40
|
|
|
41
|
+
# @return [Integer]
|
|
42
|
+
attr_accessor :custom_events
|
|
43
|
+
|
|
41
44
|
# @param request [Rack::Request] a Request object that implements the
|
|
42
45
|
# Rack::Request API, to which we will delegate behavior.
|
|
43
46
|
# @param settings [Aikido::Zen::RuntimeSettings]
|
|
@@ -45,16 +48,20 @@ module Aikido::Zen
|
|
|
45
48
|
# @yieldparam request [Rack::Request] the given request object.
|
|
46
49
|
# @yieldreturn [Hash<Symbol, #flat_map>] map of payload source types
|
|
47
50
|
# to the actual data from the request to populate them.
|
|
48
|
-
def initialize(request,
|
|
51
|
+
def initialize(request, zen: Aikido::Zen, config: zen.config, settings: zen.runtime_settings, &sources)
|
|
49
52
|
@request = request
|
|
53
|
+
@config = config
|
|
50
54
|
@settings = settings
|
|
51
55
|
@payload_sources = sources
|
|
52
56
|
|
|
57
|
+
@max_depth = @config.extract_payloads_max_depth
|
|
58
|
+
|
|
53
59
|
@metadata = {}
|
|
54
60
|
@scanning = false
|
|
55
61
|
@request_bypassed = nil
|
|
56
62
|
@protection_disabled = false
|
|
57
63
|
@idor_protection_enabled = false
|
|
64
|
+
@custom_events = 0
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
# Fetch some metadata stored in the Context.
|
|
@@ -102,31 +109,46 @@ module Aikido::Zen
|
|
|
102
109
|
private
|
|
103
110
|
|
|
104
111
|
# @!visibility private
|
|
105
|
-
def extract_payloads_from(data, source_type, prefix = nil)
|
|
112
|
+
def extract_payloads_from(data, source_type, prefix = nil, depth = 0)
|
|
113
|
+
return [] if depth > @max_depth
|
|
114
|
+
|
|
106
115
|
if data.is_a?(String)
|
|
107
116
|
[Payload.new(data, source_type, prefix.to_s)]
|
|
108
117
|
elsif data.respond_to?(:to_hash)
|
|
109
118
|
data.to_hash.flat_map do |key, value|
|
|
110
|
-
extract_payloads_from(value, source_type, [prefix, key].compact.join("."))
|
|
119
|
+
extract_payloads_from(value, source_type, [prefix, key].compact.join("."), depth + 1)
|
|
111
120
|
end
|
|
112
121
|
elsif data.respond_to?(:to_ary)
|
|
113
122
|
array = data.to_ary
|
|
114
123
|
return array if array.empty?
|
|
115
124
|
|
|
116
125
|
payloads = array.flat_map.with_index do |value, index|
|
|
117
|
-
extract_payloads_from(value, source_type, [prefix, index].compact.join("."))
|
|
126
|
+
extract_payloads_from(value, source_type, [prefix, index].compact.join("."), depth + 1)
|
|
118
127
|
end
|
|
119
128
|
|
|
120
129
|
unless Aikido::Zen.config.harden?
|
|
121
130
|
# Special case for File.join given a possibly nested array of strings,
|
|
122
131
|
# as might occur when a query parameter is an array.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
|
|
133
|
+
# File.join recursively joins nested string arrays, and can overflow
|
|
134
|
+
# the stack given deeply nested arrays.
|
|
135
|
+
#
|
|
136
|
+
# Flatten array to max depth and check that the flattened array is an
|
|
137
|
+
# array of strings before calling File.join__internal_for_aikido_zen,
|
|
138
|
+
# only if the array was fully flattened, to prevent a stack overflow.
|
|
139
|
+
#
|
|
140
|
+
# Checking that all values are Strings handily prevents a TypeError
|
|
141
|
+
# from being raised.
|
|
142
|
+
flattened_array = array.flatten(@max_depth)
|
|
143
|
+
if flattened_array.all? { |string| string.is_a?(String) }
|
|
144
|
+
begin
|
|
145
|
+
string = File.join__internal_for_aikido_zen(*flattened_array)
|
|
146
|
+
if unsafe_path?(string)
|
|
147
|
+
payloads << Payload.new(string, source_type, [prefix, "__File.join__"].compact.join("."))
|
|
148
|
+
end
|
|
149
|
+
rescue
|
|
150
|
+
# Could not create special payload for File.join.
|
|
127
151
|
end
|
|
128
|
-
rescue
|
|
129
|
-
# Could not create special payload for File.join.
|
|
130
152
|
end
|
|
131
153
|
end
|
|
132
154
|
|
data/lib/aikido/zen/helpers.rb
CHANGED
|
@@ -29,6 +29,22 @@ module Aikido
|
|
|
29
29
|
|
|
30
30
|
Regexp.new(regexp.source, regexp.options, timeout: timeout)
|
|
31
31
|
end
|
|
32
|
+
|
|
33
|
+
# Returns a copy of the string safely encoded as UTF-8.
|
|
34
|
+
#
|
|
35
|
+
# Web servers may deliver request data as binary strings. These strings
|
|
36
|
+
# are relabeled as UTF-8 instead of being converted, because conversion
|
|
37
|
+
# would replace every non-ASCII byte with U+FFFD, even in valid UTF-8.
|
|
38
|
+
#
|
|
39
|
+
# @param string [String] the string
|
|
40
|
+
# @return [String] the valid UTF-8 string
|
|
41
|
+
def self.encode_safely(string)
|
|
42
|
+
if string.encoding == Encoding::BINARY
|
|
43
|
+
string.dup.force_encoding(Encoding::UTF_8).scrub
|
|
44
|
+
else
|
|
45
|
+
string.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
32
48
|
end
|
|
33
49
|
end
|
|
34
50
|
end
|
data/lib/aikido/zen/internals.rb
CHANGED
|
@@ -103,8 +103,8 @@ module Aikido::Zen
|
|
|
103
103
|
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
|
104
104
|
# calling libzen.
|
|
105
105
|
def self.detect_sql_injection(query, input, dialect)
|
|
106
|
-
query_bytes = encode_safely(query)
|
|
107
|
-
input_bytes = encode_safely(input)
|
|
106
|
+
query_bytes = Aikido::Zen::Helpers.encode_safely(query)
|
|
107
|
+
input_bytes = Aikido::Zen::Helpers.encode_safely(input)
|
|
108
108
|
|
|
109
109
|
query_ptr = FFI::MemoryPointer.new(:uint8, query_bytes.bytesize)
|
|
110
110
|
input_ptr = FFI::MemoryPointer.new(:uint8, input_bytes.bytesize)
|
|
@@ -123,7 +123,7 @@ module Aikido::Zen
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def self.idor_analyze_sql(query, dialect)
|
|
126
|
-
query_bytes = encode_safely(query)
|
|
126
|
+
query_bytes = Aikido::Zen::Helpers.encode_safely(query)
|
|
127
127
|
query_ptr = FFI::MemoryPointer.new(:uint8, query_bytes.bytesize)
|
|
128
128
|
query_ptr.put_bytes(0, query_bytes)
|
|
129
129
|
|
|
@@ -134,13 +134,5 @@ module Aikido::Zen
|
|
|
134
134
|
JSON.parse(result_json)
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
|
-
|
|
138
|
-
class << self
|
|
139
|
-
private
|
|
140
|
-
|
|
141
|
-
def encode_safely(string)
|
|
142
|
-
string.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
137
|
end
|
|
146
138
|
end
|
data/lib/aikido/zen/payload.rb
CHANGED
|
@@ -24,10 +24,13 @@ module Aikido::Zen
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def as_json
|
|
27
|
+
safe_value = Aikido::Zen::Helpers.encode_safely(value.to_s)
|
|
28
|
+
safe_path = Aikido::Zen::Helpers.encode_safely(path.to_s)
|
|
29
|
+
|
|
27
30
|
{
|
|
28
|
-
"payload" =>
|
|
31
|
+
"payload" => safe_value,
|
|
29
32
|
"source" => SOURCE_SERIALIZATIONS[source],
|
|
30
|
-
"path" => ".#{
|
|
33
|
+
"path" => ".#{safe_path}"
|
|
31
34
|
}
|
|
32
35
|
end
|
|
33
36
|
|
data/lib/aikido/zen/request.rb
CHANGED
|
@@ -17,6 +17,11 @@ module Aikido::Zen
|
|
|
17
17
|
# @see Aikido::Zen.track_user
|
|
18
18
|
attr_accessor :actor
|
|
19
19
|
|
|
20
|
+
# The current rate limiting group, if set by the host app.
|
|
21
|
+
#
|
|
22
|
+
# @return [String, nil]
|
|
23
|
+
attr_accessor :rate_limiting_group
|
|
24
|
+
|
|
20
25
|
# The current tenant, if set by the host app.
|
|
21
26
|
#
|
|
22
27
|
# @return [Integer, String, nil]
|
|
@@ -86,13 +91,17 @@ module Aikido::Zen
|
|
|
86
91
|
end
|
|
87
92
|
|
|
88
93
|
def as_json
|
|
94
|
+
safe_url = Aikido::Zen::Helpers.encode_safely(url)
|
|
95
|
+
safe_user_agent = Aikido::Zen::Helpers.encode_safely(user_agent) if user_agent
|
|
96
|
+
safe_route_path = Aikido::Zen::Helpers.encode_safely(route.path) if route&.path
|
|
97
|
+
|
|
89
98
|
{
|
|
90
99
|
"method" => request_method.upcase,
|
|
91
|
-
"url" =>
|
|
100
|
+
"url" => safe_url,
|
|
92
101
|
"ipAddress" => client_ip,
|
|
93
|
-
"userAgent" =>
|
|
102
|
+
"userAgent" => safe_user_agent,
|
|
94
103
|
"source" => framework,
|
|
95
|
-
"route" =>
|
|
104
|
+
"route" => safe_route_path
|
|
96
105
|
}
|
|
97
106
|
end
|
|
98
107
|
|
|
@@ -40,8 +40,8 @@ module Aikido::Zen
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def initialize(filepath, input)
|
|
43
|
-
@filepath = filepath.downcase
|
|
44
|
-
@input = input.downcase
|
|
43
|
+
@filepath = Aikido::Zen::Helpers.encode_safely(filepath).downcase
|
|
44
|
+
@input = Aikido::Zen::Helpers.encode_safely(input).downcase
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def attack?
|
|
@@ -37,8 +37,8 @@ module Aikido::Zen
|
|
|
37
37
|
# @param command [String]
|
|
38
38
|
# @param input [String]
|
|
39
39
|
def initialize(command, input)
|
|
40
|
-
@command = command
|
|
41
|
-
@input = input
|
|
40
|
+
@command = Aikido::Zen::Helpers.encode_safely(command)
|
|
41
|
+
@input = Aikido::Zen::Helpers.encode_safely(input)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def attack?
|
|
@@ -53,8 +53,8 @@ module Aikido::Zen
|
|
|
53
53
|
attr_reader :failed_to_tokenize
|
|
54
54
|
|
|
55
55
|
def initialize(query, input, dialect)
|
|
56
|
-
@query = query.downcase
|
|
57
|
-
@input = input.downcase.strip
|
|
56
|
+
@query = Aikido::Zen::Helpers.encode_safely(query).downcase
|
|
57
|
+
@input = Aikido::Zen::Helpers.encode_safely(input).downcase.strip
|
|
58
58
|
@dialect = dialect
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -85,9 +85,11 @@ module Aikido::Zen
|
|
|
85
85
|
# Matches strings that contain only characters that appear in some
|
|
86
86
|
# form of IP address.
|
|
87
87
|
#
|
|
88
|
+
# An IPv6 zone ID (e.g. "%eth0") may follow the address.
|
|
89
|
+
#
|
|
88
90
|
# Hostnames are not expected to match, allowing `parse_address` to
|
|
89
91
|
# skip the `Socket.getaddrinfo` call.
|
|
90
|
-
ADDRESS_REGEXP = /\A[0-9a-fx:.]
|
|
92
|
+
ADDRESS_REGEXP = /\A[0-9a-fx:.]+(?:%[^%\s]+)?\z/i
|
|
91
93
|
|
|
92
94
|
# Parses `address` as an IP address, in any form that is accepted
|
|
93
95
|
# by `getaddrinfo`:
|
|
@@ -96,6 +98,7 @@ module Aikido::Zen
|
|
|
96
98
|
# * Plain integer IPv4 notation, in decimal, octal, or hexadecimal
|
|
97
99
|
# (e.g. "2130706433", "017700000001", "0x7f000001")
|
|
98
100
|
# * Shorthand dotted IPv4 notation (e.g. "127.1")
|
|
101
|
+
# * IPv6 notation with zone ID (e.g. "fe80::1%eth0")
|
|
99
102
|
#
|
|
100
103
|
# Delegates to `Socket.getaddrinfo` with the `AI_NUMERICHOST` flag,
|
|
101
104
|
# which never performs a DNS lookup.
|
|
@@ -105,8 +108,10 @@ module Aikido::Zen
|
|
|
105
108
|
def parse_address(address)
|
|
106
109
|
return nil unless address.is_a?(String) && ADDRESS_REGEXP.match?(address)
|
|
107
110
|
|
|
108
|
-
Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST)
|
|
109
|
-
.
|
|
111
|
+
Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST).map do |info|
|
|
112
|
+
# `IPAddr.new` only accepts a zone ID on Ruby 3.1+.
|
|
113
|
+
IPAddr.new(info[3].partition("%").first)
|
|
114
|
+
end
|
|
110
115
|
rescue SocketError
|
|
111
116
|
nil
|
|
112
117
|
end
|
|
@@ -146,7 +146,9 @@ module Aikido::Zen
|
|
|
146
146
|
#
|
|
147
147
|
# @return [Array<URI>] a list of unique URIs based on the above criteria.
|
|
148
148
|
def uris_from_input
|
|
149
|
-
input
|
|
149
|
+
# Percent-encode input so URI() can parse it, as an app may do before
|
|
150
|
+
# making the request.
|
|
151
|
+
input = percent_encode(@input.to_s)
|
|
150
152
|
|
|
151
153
|
# If you build a URI manually and set the hostname to an IPv6 string,
|
|
152
154
|
# the URI library will be helpful to wrap it in brackets so it's a
|
|
@@ -167,6 +169,17 @@ module Aikido::Zen
|
|
|
167
169
|
nil
|
|
168
170
|
end
|
|
169
171
|
|
|
172
|
+
# Matches bytes not allowed in a URI (RFC 3986): any byte other than
|
|
173
|
+
# the unreserved and reserved characters, and any "%" that does not
|
|
174
|
+
# start a percent-encoded byte.
|
|
175
|
+
NOT_ALLOWED_IN_URI = /%(?![0-9A-Fa-f]{2})|[^A-Za-z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]/n
|
|
176
|
+
|
|
177
|
+
# Percent-encodes characters not allowed in a URI, preserving existing
|
|
178
|
+
# percent-encoding.
|
|
179
|
+
def percent_encode(string)
|
|
180
|
+
string.b.gsub(NOT_ALLOWED_IN_URI) { |byte| format("%%%02X", byte.ord) }
|
|
181
|
+
end
|
|
182
|
+
|
|
170
183
|
# Check if the input is an IPv6 that is not surrounded by square brackets.
|
|
171
184
|
def unescaped_ipv6?(input)
|
|
172
185
|
(
|
data/lib/aikido/zen/version.rb
CHANGED
|
@@ -64,7 +64,10 @@ module Aikido::Zen::WorkerProcess
|
|
|
64
64
|
def calculate_rate_limits(request)
|
|
65
65
|
result = @rpc_client.invoke(
|
|
66
66
|
"calculate_rate_limits", Aikido::Zen::IPC::READ_TIMEOUT,
|
|
67
|
-
request.route.as_json,
|
|
67
|
+
request.route.as_json,
|
|
68
|
+
request.client_ip,
|
|
69
|
+
request.actor&.as_json,
|
|
70
|
+
request.rate_limiting_group
|
|
68
71
|
)
|
|
69
72
|
|
|
70
73
|
Aikido::Zen::RateLimiter::Result.from_json(result) if result
|
|
@@ -22,8 +22,8 @@ module Aikido::Zen::WorkerProcess
|
|
|
22
22
|
send_collector_events(events_data)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
@rpc_server.handle("calculate_rate_limits") do |respond, route_data, ip, actor_data|
|
|
26
|
-
result = calculate_rate_limits(route_data, ip, actor_data)
|
|
25
|
+
@rpc_server.handle("calculate_rate_limits") do |respond, route_data, ip, actor_data, rate_limiting_group|
|
|
26
|
+
result = calculate_rate_limits(route_data, ip, actor_data, rate_limiting_group)
|
|
27
27
|
respond.call(result&.as_json)
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -66,7 +66,7 @@ module Aikido::Zen::WorkerProcess
|
|
|
66
66
|
|
|
67
67
|
# @api private
|
|
68
68
|
# @note Visible for testing.
|
|
69
|
-
RequestKind = Struct.new(:route, :schema, :client_ip, :actor)
|
|
69
|
+
RequestKind = Struct.new(:route, :schema, :client_ip, :actor, :rate_limiting_group)
|
|
70
70
|
|
|
71
71
|
def updated_settings(known_config_generation = nil, known_firewall_lists_generation = nil)
|
|
72
72
|
result = {}
|
|
@@ -91,10 +91,10 @@ module Aikido::Zen::WorkerProcess
|
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
def calculate_rate_limits(route_data, ip, actor_data)
|
|
94
|
+
def calculate_rate_limits(route_data, ip, actor_data, rate_limiting_group)
|
|
95
95
|
actor = Aikido::Zen::Actor.from_json(actor_data) if actor_data
|
|
96
96
|
route = Aikido::Zen::Route.from_json(route_data)
|
|
97
|
-
Aikido::Zen.rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor))
|
|
97
|
+
Aikido::Zen.rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor, rate_limiting_group))
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
# @return [Array<Hash>, nil]
|
data/lib/aikido/zen.rb
CHANGED
|
@@ -278,6 +278,37 @@ module Aikido
|
|
|
278
278
|
alias_method :set_user, :track_user
|
|
279
279
|
end
|
|
280
280
|
|
|
281
|
+
# Set the rate limiting group.
|
|
282
|
+
#
|
|
283
|
+
# @param group [String, Integer] the rate limiting group id
|
|
284
|
+
# @return [void]
|
|
285
|
+
def self.set_rate_limiting_group(group)
|
|
286
|
+
return if config.disabled?
|
|
287
|
+
|
|
288
|
+
if group.nil? || group == ""
|
|
289
|
+
config.logger.warn("set_rate_limiting_group expects a non-empty group")
|
|
290
|
+
return
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
unless group.is_a?(String) || group.is_a?(Integer)
|
|
294
|
+
config.logger.warn("set_rate_limiting_group expects a String or Integer group, got #{group.class}")
|
|
295
|
+
return
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
context = current_context
|
|
299
|
+
if context.nil?
|
|
300
|
+
config.logger.warn("set_rate_limiting_group was called without a context")
|
|
301
|
+
return
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
context.request.rate_limiting_group = group.to_s
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Align with other Zen implementations, while keeping internal consistency.
|
|
308
|
+
class << self
|
|
309
|
+
alias_method :set_rate_limit_group, :set_rate_limiting_group
|
|
310
|
+
end
|
|
311
|
+
|
|
281
312
|
# Track a custom event happening in your application, like a failed login or
|
|
282
313
|
# a password reset request. Zen automatically attaches the IP address, user
|
|
283
314
|
# agent, and current user (if you called .track_user) from the request.
|
|
@@ -300,6 +331,13 @@ module Aikido
|
|
|
300
331
|
|
|
301
332
|
return if request_bypassed?
|
|
302
333
|
|
|
334
|
+
if context.custom_events >= config.max_custom_events
|
|
335
|
+
config.logger.warn("Dropped custom event #{name}: too many custom events were tracked for this request")
|
|
336
|
+
return
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
context.custom_events += 1
|
|
340
|
+
|
|
303
341
|
event = Events::Custom.new(
|
|
304
342
|
name: name,
|
|
305
343
|
request: context.request,
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aikido-zen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.0.beta.
|
|
4
|
+
version: 1.9.0.beta.4
|
|
5
5
|
platform: arm64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Aikido Security
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|