aikido-zen 1.3.0 → 1.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da6cdcfab4ab64c1d5f25d652fe71da5f869e32516faffec48ad4c542abb2aec
4
- data.tar.gz: 87ff936a996e0f4b4cd459cd4293cb60853c6f8f26f7b5b61ef698600e4921a9
3
+ metadata.gz: bfcc07a5e3256e9d9d35f89bea1cf884d2c51bf23d497a40401f61d7335a3dbc
4
+ data.tar.gz: 81055a97f6a640bafb3bfd29fbc96f0c0eeb165046191bd68518109908d92817
5
5
  SHA512:
6
- metadata.gz: 42e79d8f15a8bdb3ec3fb4fe7b83648ae86784ee3c52a383b2052a7ed28957506aac56a1d90b18f5a0baa0eef65ea449aa46c0fde95a87e57ffbd7382fcf1b55
7
- data.tar.gz: 979ee37703c1198f9a3b261819410e88ad53542e13a80b18cfe9458b5088fab5ce7cda228ef54ba01db2277eeda94db15bc23c396b0fa718fb9bf3f8cd7f3971
6
+ metadata.gz: dba14387480fa74530c2120e123aa62c9e9b81054fa58149f26a5e4d6aeca4da5fc96732add6313ba02d3fecc5c6c8a587a208e47916e2a136a277159a86b429
7
+ data.tar.gz: 523e4fe6c7b14586a5d9e893ad51407fe6c46c52c408eea730ad8f45bab544d5861198a8e8fa5f6e6e19c69bc532bacdadcf1da4e8c74c6e89549c3f3108e68e
data/.simplecov CHANGED
@@ -3,7 +3,7 @@
3
3
  # Due to dependency resolution, on Ruby 2.x we're stuck with a _very_ old
4
4
  # SimpleCov version, and it doesn't really give us any benefit to run coverage
5
5
  # in separate ruby versions since we don't branch on ruby version in the code.
6
- return if RUBY_VERSION < "3.0"
6
+ return if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
7
7
  return if ENV["DISABLE_COVERAGE"] == "true"
8
8
 
9
9
  # Output coverage as LCOV to support CodeCov
@@ -0,0 +1,9 @@
1
+ # Blocking invalid SQL queries
2
+
3
+ Zen blocks SQL queries that it can't tokenize when they contain user input. This prevents attackers from bypassing SQL injection detection with malformed queries. For example, ClickHouse ignores invalid SQL after `;`, and SQLite runs queries before an unclosed `/*` comment.
4
+
5
+ This is on by default. In blocking mode, these queries are blocked. In detection-only mode, they are reported but still executed.
6
+
7
+ If you see false positives (legitimate queries being blocked), set
8
+ `AIKIDO_BLOCK_INVALID_SQL=false` in your environment, or set
9
+ `Aikido::Zen.config.block_invalid_sql = false`.
@@ -121,11 +121,12 @@ module Aikido::Zen
121
121
  attr_reader :input
122
122
  attr_reader :dialect
123
123
 
124
- def initialize(query:, input:, dialect:, **opts)
124
+ def initialize(query:, input:, dialect:, failed_to_tokenize:, **opts)
125
125
  super(**opts)
126
126
  @query = query
127
127
  @input = input
128
128
  @dialect = dialect
129
+ @failed_to_tokenize = failed_to_tokenize
129
130
  end
130
131
 
131
132
  def humanized_name
@@ -139,8 +140,9 @@ module Aikido::Zen
139
140
  def metadata
140
141
  {
141
142
  sql: @query,
142
- dialect: @dialect.name
143
- }
143
+ dialect: @dialect.name,
144
+ failedToTokenize: @failed_to_tokenize || nil
145
+ }.compact
144
146
  end
145
147
 
146
148
  def exception(*)
@@ -28,6 +28,14 @@ module Aikido::Zen
28
28
  attr_accessor :blocking_mode
29
29
  alias_method :blocking_mode?, :blocking_mode
30
30
 
31
+ # @return [Boolean] is the agent in debugging mode?
32
+ attr_accessor :debugging
33
+ alias_method :debugging?, :debugging
34
+
35
+ # @return [String] the token obtained when configuring the Firewall in the
36
+ # Aikido interface.
37
+ attr_accessor :api_token
38
+
31
39
  # @return [URI] The HTTP host for the Aikido API. Defaults to
32
40
  # +https://guard.aikido.dev+.
33
41
  attr_reader :api_endpoint
@@ -39,10 +47,6 @@ module Aikido::Zen
39
47
  # @return [Hash] HTTP timeouts for communicating with the API.
40
48
  attr_reader :api_timeouts
41
49
 
42
- # @return [String] the token obtained when configuring the Firewall in the
43
- # Aikido interface.
44
- attr_accessor :api_token
45
-
46
50
  # @return [Integer] the interval in seconds to poll the runtime API for
47
51
  # settings changes. Defaults to evey 60 seconds.
48
52
  attr_accessor :polling_interval
@@ -67,10 +71,6 @@ module Aikido::Zen
67
71
  # Defaults to `aikido-detached-agent.sock`.
68
72
  attr_accessor :detached_agent_socket_path
69
73
 
70
- # @return [Boolean] is the agent in debugging mode?
71
- attr_accessor :debugging
72
- alias_method :debugging?, :debugging
73
-
74
74
  # @return [String] environment specific HTTP header providing the client IP.
75
75
  attr_accessor :client_ip_header
76
76
 
@@ -165,6 +165,12 @@ module Aikido::Zen
165
165
  attr_accessor :harden
166
166
  alias_method :harden?, :harden
167
167
 
168
+ # @return [Boolean] whether Aikido Zen should block SQL queries that fail
169
+ # tokenization when user input is present. Defaults to false. Can be set
170
+ # through AIKIDO_BLOCK_INVALID_SQL environment variable.
171
+ attr_accessor :block_invalid_sql
172
+ alias_method :block_invalid_sql?, :block_invalid_sql
173
+
168
174
  # @return [Integer] how many suspicious requests are allowed before an
169
175
  # attack wave detected event is reported.
170
176
  # Defaults to 15 requests.
@@ -188,19 +194,24 @@ module Aikido::Zen
188
194
  # Defaults to 15 entries.
189
195
  attr_accessor :attack_wave_max_cache_samples
190
196
 
197
+ # @return [Float, nil] the timeout in seconds for regular expression matching.
198
+ # Applied to selected internal regular expressions to mitigate ReDoS risks.
199
+ # Defaults to 1.0 seconds.
200
+ attr_accessor :redos_regexp_timeout
201
+
191
202
  def initialize
192
203
  self.insert_middleware_after = ::ActionDispatch::RemoteIp
193
204
  self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
194
205
  self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
195
- self.api_timeouts = 10
206
+ self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
207
+ self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
196
208
  self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT", DEFAULT_AIKIDO_ENDPOINT)
197
209
  self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
198
- self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
210
+ self.api_timeouts = 10
199
211
  self.polling_interval = 60 # 1 min
200
212
  self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
201
213
  self.json_encoder = DEFAULT_JSON_ENCODER
202
214
  self.json_decoder = DEFAULT_JSON_DECODER
203
- self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
204
215
  self.logger = Logger.new($stdout, progname: "aikido", level: debugging ? Logger::DEBUG : Logger::INFO)
205
216
  self.detached_agent_socket_path = ENV.fetch("AIKIDO_DETACHED_AGENT_SOCKET_PATH", DEFAULT_DETACHED_AGENT_SOCKET_PATH)
206
217
  self.client_ip_header = ENV.fetch("AIKIDO_CLIENT_IP_HEADER", nil)
@@ -208,25 +219,27 @@ module Aikido::Zen
208
219
  self.max_compressed_stats = 100
209
220
  self.max_outbound_connections = 200
210
221
  self.max_users_tracked = 1000
211
- self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
212
222
  self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
213
223
  self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
214
224
  self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
215
- self.server_rate_limit_deadline = 30 * 60 # 30 min
216
- self.client_rate_limit_period = 60 * 60 # 1 hour
217
- self.client_rate_limit_max_events = 100
218
225
  self.collect_api_schema = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", true))
219
226
  self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
220
227
  self.api_schema_collection_max_depth = 20
221
228
  self.api_schema_collection_max_properties = 20
229
+ self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
230
+ self.client_rate_limit_period = 60 * 60 # 1 hour
231
+ self.client_rate_limit_max_events = 100
232
+ self.server_rate_limit_deadline = 30 * 60 # 30 min
222
233
  self.stored_ssrf = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_STORED_SSRF", true))
223
234
  self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
224
235
  self.harden = read_boolean_from_env(ENV.fetch("AIKIDO_HARDEN", true))
236
+ self.block_invalid_sql = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK_INVALID_SQL", false))
225
237
  self.attack_wave_threshold = 15
226
238
  self.attack_wave_min_time_between_requests = 60 * 1000 # 1 min (ms)
227
239
  self.attack_wave_min_time_between_events = 20 * 60 * 1000 # 20 min (ms)
228
240
  self.attack_wave_max_cache_entries = 10_000
229
241
  self.attack_wave_max_cache_samples = 15
242
+ self.redos_regexp_timeout = 1.0
230
243
  end
231
244
 
232
245
  # Set the base URL for API requests.
@@ -14,12 +14,36 @@ module Aikido::Zen
14
14
  request = Aikido::Zen::Request.new(delegate, framework: "rack", router: router)
15
15
 
16
16
  Context.new(request) do |req|
17
+ query = begin
18
+ req.GET
19
+ rescue
20
+ {}
21
+ end
22
+
23
+ body = begin
24
+ req.POST
25
+ rescue
26
+ {}
27
+ end
28
+
29
+ header = begin
30
+ req.normalized_headers
31
+ rescue
32
+ {}
33
+ end
34
+
35
+ cookie = begin
36
+ req.cookies
37
+ rescue
38
+ {}
39
+ end
40
+
17
41
  {
18
- query: req.GET,
19
- body: req.POST,
42
+ query: query,
43
+ body: body,
20
44
  route: {},
21
- header: req.normalized_headers,
22
- cookie: req.cookies,
45
+ header: header,
46
+ cookie: cookie,
23
47
  subdomain: []
24
48
  }
25
49
  end
@@ -34,13 +34,49 @@ module Aikido::Zen
34
34
  end
35
35
 
36
36
  Context.new(request) do |req|
37
+ query = begin
38
+ req.query_parameters
39
+ rescue
40
+ {}
41
+ end
42
+
43
+ body = begin
44
+ req.request_parameters
45
+ rescue
46
+ {}
47
+ end
48
+
49
+ route = begin
50
+ req.path_parameters
51
+ rescue
52
+ {}
53
+ end
54
+
55
+ header = begin
56
+ req.normalized_headers
57
+ rescue
58
+ {}
59
+ end
60
+
61
+ cookie = begin
62
+ decrypt_cookies.call(req)
63
+ rescue
64
+ {}
65
+ end
66
+
67
+ subdomain = begin
68
+ req.subdomains
69
+ rescue
70
+ []
71
+ end
72
+
37
73
  {
38
- query: req.query_parameters,
39
- body: req.request_parameters,
40
- route: req.path_parameters,
41
- header: req.normalized_headers,
42
- cookie: decrypt_cookies.call(req),
43
- subdomain: req.subdomains
74
+ query: query,
75
+ body: body,
76
+ route: route,
77
+ header: header,
78
+ cookie: cookie,
79
+ subdomain: subdomain
44
80
  }
45
81
  end
46
82
  end
@@ -19,6 +19,16 @@ module Aikido
19
19
  normalized_path.chomp!("/") unless normalized_path == "/"
20
20
  normalized_path
21
21
  end
22
+
23
+ # Returns a copy of the regexp with the timeout set if timeout is supported.
24
+ #
25
+ # @param regexp [Regexp] the regexp
26
+ # @return [Regexp] the regexp with timeout set
27
+ def self.regexp_with_timeout(regexp, timeout: Aikido::Zen.config.redos_regexp_timeout)
28
+ return regexp if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2")
29
+
30
+ Regexp.new(regexp.source, regexp.options, timeout: timeout)
31
+ end
22
32
  end
23
33
  end
24
34
  end
@@ -99,16 +99,14 @@ module Aikido::Zen
99
99
  query_ptr.put_bytes(0, query_bytes)
100
100
  input_ptr.put_bytes(0, input_bytes)
101
101
 
102
- case detect_sql_injection_native(query_ptr, query_bytes.bytesize, input_ptr, input_bytes.bytesize, dialect)
103
- when 0 then false
104
- when 1 then true
105
- when 2
102
+ result = detect_sql_injection_native(query_ptr, query_bytes.bytesize, input_ptr, input_bytes.bytesize, dialect)
103
+
104
+ if result == 2
106
105
  attempt = format("%s query %p with input %p", dialect, query, input)
107
106
  raise InternalsError.new(attempt, "calling detect_sql_injection in", libzen_name)
108
- when 3
109
- # SQL tokenization failed - return false (no injection detected)
110
- false
111
107
  end
108
+
109
+ result
112
110
  end
113
111
  end
114
112
 
@@ -26,7 +26,7 @@ module Aikido::Zen
26
26
 
27
27
  def __setobj__(delegate) # :nodoc:
28
28
  super
29
- @route = @normalized_header = nil
29
+ @route = @normalized_headers = nil
30
30
  end
31
31
 
32
32
  # @return [Aikido::Zen::Route] the framework route being requested.
@@ -64,10 +64,14 @@ module Aikido::Zen
64
64
  def normalized_headers
65
65
  @normalized_headers ||= env.slice(*BLESSED_CGI_HEADERS)
66
66
  .merge(env.select { |key, _| key.start_with?("HTTP_") })
67
- .transform_keys { |header|
68
- name = header.sub(/^HTTP_/, "").downcase
69
- name.split("_").map { |part| part[0].upcase + part[1..] }.join("-")
70
- }
67
+ .transform_keys do |header|
68
+ header
69
+ .delete_prefix("HTTP_")
70
+ .downcase
71
+ .split("_")
72
+ .map(&:capitalize)
73
+ .join("-")
74
+ end
71
75
  end
72
76
 
73
77
  def as_json
@@ -13,13 +13,14 @@ module Aikido::Zen
13
13
  # Path Traversal kind of attacks.
14
14
  #
15
15
  # @param filepath [String] the expanded path that is tried to be read
16
- # @param context [Aikido::Zen::Context]
16
+ # @param scan [Aikido::Zen::Scan] the running scan.
17
17
  # @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
18
+ # @param context [Aikido::Zen::Context]
18
19
  # @param operation [Symbol, String] name of the method being scanned.
19
20
  #
20
21
  # @return [Aikido::Zen::Attacks::PathTraversalAttack, nil] an Attack if any
21
22
  # user input is detected to be attempting a Path Traversal Attack, or +nil+ if not.
22
- def self.call(filepath:, sink:, context:, operation:)
23
+ def self.call(scan:, sink:, context:, filepath:, operation:)
23
24
  context.payloads.each do |payload|
24
25
  next unless new(filepath, payload.value.to_s).attack?
25
26
 
@@ -31,6 +32,8 @@ module Aikido::Zen
31
32
  operation: "#{sink.operation}.#{operation}",
32
33
  stack: Aikido::Zen.clean_stack_trace
33
34
  )
35
+ rescue => error
36
+ scan.track_error(error, self)
34
37
  end
35
38
 
36
39
  nil
@@ -10,11 +10,12 @@ module Aikido::Zen
10
10
  end
11
11
 
12
12
  # @param command [String]
13
+ # @param scan [Aikido::Zen::Scan]
13
14
  # @param sink [Aikido::Zen::Sink]
14
15
  # @param context [Aikido::Zen::Context]
15
16
  # @param operation [Symbol, String]
16
17
  #
17
- def self.call(command:, sink:, context:, operation:)
18
+ def self.call(command:, scan:, sink:, context:, operation:)
18
19
  context.payloads.each do |payload|
19
20
  next unless new(command, payload.value.to_s).attack?
20
21
 
@@ -26,6 +27,8 @@ module Aikido::Zen
26
27
  operation: "#{sink.operation}.#{operation}",
27
28
  stack: Aikido::Zen.clean_stack_trace
28
29
  )
30
+ rescue => error
31
+ scan.track_error(error, self)
29
32
  end
30
33
 
31
34
  nil
@@ -14,25 +14,24 @@ module Aikido::Zen
14
14
  # and returns an Attack if so, based on the current request.
15
15
  #
16
16
  # @param query [String]
17
- # @param context [Aikido::Zen::Context]
18
- # @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
19
17
  # @param dialect [Symbol] one of +:mysql+, +:postgesql+, or +:sqlite+.
18
+ # @param scan [Aikido::Zen::Scan] the running scan.
19
+ # @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
20
+ # @param context [Aikido::Zen::Context]
20
21
  # @param operation [Symbol, String] name of the method being scanned.
21
22
  # Expects +sink.operation+ being set to get the full module/name combo.
22
23
  #
23
24
  # @return [Aikido::Zen::Attack, nil] an Attack if any user input is
24
25
  # detected to be attempting a SQL injection, or nil if this is safe.
25
- #
26
- # @raise [Aikido::Zen::InternalsError] if an error occurs when loading or
27
- # calling zenlib. See Sink#scan.
28
- def self.call(query:, dialect:, sink:, context:, operation:)
26
+ def self.call(query:, dialect:, scan:, sink:, context:, operation:)
29
27
  dialect = DIALECTS.fetch(dialect) do
30
28
  Aikido::Zen.config.logger.warn "Unknown SQL dialect #{dialect.inspect}"
31
29
  DIALECTS[:common]
32
30
  end
33
31
 
34
32
  context.payloads.each do |payload|
35
- next unless new(query, payload.value.to_s, dialect).attack?
33
+ scanner = new(query, payload.value.to_s, dialect)
34
+ next unless scanner.attack?
36
35
 
37
36
  return Attacks::SQLInjectionAttack.new(
38
37
  sink: sink,
@@ -41,13 +40,21 @@ module Aikido::Zen
41
40
  dialect: dialect,
42
41
  context: context,
43
42
  operation: "#{sink.operation}.#{operation}",
44
- stack: Aikido::Zen.clean_stack_trace
43
+ stack: Aikido::Zen.clean_stack_trace,
44
+ failed_to_tokenize: scanner.failed_to_tokenize
45
45
  )
46
+ rescue Aikido::Zen::InternalsError => error
47
+ Aikido::Zen.config.logger.warn(error.message)
48
+ scan.track_error(error, self)
49
+ rescue => error
50
+ scan.track_error(error, self)
46
51
  end
47
52
 
48
53
  nil
49
54
  end
50
55
 
56
+ attr_reader :failed_to_tokenize
57
+
51
58
  def initialize(query, input, dialect)
52
59
  @query = query.downcase
53
60
  @input = input.downcase
@@ -65,12 +72,26 @@ module Aikido::Zen
65
72
  return false unless @query.include?(@input)
66
73
 
67
74
  # If the input is solely alphanumeric, we can ignore it
68
- return false if /\A[[:alnum:]_]+\z/i.match?(@input)
75
+ return false if Aikido::Zen::Helpers.regexp_with_timeout(/\A[[:alnum:]_]+\z/i).match?(@input)
69
76
 
70
77
  # If the input is a comma-separated list of numbers, ignore it.
71
- return false if /\A(?:\d+(?:,\s*)?)+\z/i.match?(@input)
78
+ return false if Aikido::Zen::Helpers.regexp_with_timeout(/\A[ ,]*\d[ ,\d]*\z/).match?(@input)
79
+
80
+ result = Internals.detect_sql_injection(@query, @input, @dialect)
81
+
82
+ case result
83
+ when 0
84
+ false
85
+ when 1
86
+ true
87
+ when 3
88
+ @failed_to_tokenize = true
89
+ Aikido::Zen.config.block_invalid_sql?
90
+ end
91
+ rescue => err
92
+ return true if defined?(Regexp::TimeoutError) && err.is_a?(Regexp::TimeoutError)
72
93
 
73
- Internals.detect_sql_injection(@query, @input, @dialect)
94
+ raise err
74
95
  end
75
96
 
76
97
  # @api private
@@ -30,14 +30,15 @@ module Aikido::Zen
30
30
  #
31
31
  # @param request [Aikido::Zen::Scanners::SSRFScanner::Request, nil]
32
32
  # the ongoing outbound HTTP request.
33
- # @param context [Aikido::Zen::Context]
33
+ # @param scan [Aikido::Zen::Scan] the running scan.
34
34
  # @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
35
+ # @param context [Aikido::Zen::Context]
35
36
  # @param operation [Symbol, String] name of the method being scanned.
36
37
  # Expects +sink.operation+ being set to get the full module/name combo.
37
38
  #
38
39
  # @return [Aikido::Zen::Attacks::SSRFAttack, nil] an Attack if any user
39
40
  # input is detected to be attempting SSRF, or +nil+ if not.
40
- def self.call(request:, sink:, context:, operation:, **)
41
+ def self.call(request:, scan:, sink:, context:, operation:, **)
41
42
  return if request.nil? # See NOTE above.
42
43
 
43
44
  context["ssrf.redirects"] ||= RedirectChains.new
@@ -46,7 +47,7 @@ module Aikido::Zen
46
47
  scanner = new(request.uri, payload.value, context["ssrf.redirects"])
47
48
  next unless scanner.attack?
48
49
 
49
- attack = Attacks::SSRFAttack.new(
50
+ return Attacks::SSRFAttack.new(
50
51
  sink: sink,
51
52
  request: request,
52
53
  input: payload,
@@ -54,8 +55,8 @@ module Aikido::Zen
54
55
  operation: "#{sink.operation}.#{operation}",
55
56
  stack: Aikido::Zen.clean_stack_trace
56
57
  )
57
-
58
- return attack
58
+ rescue => error
59
+ scan.track_error(error, self)
59
60
  end
60
61
 
61
62
  nil
@@ -109,17 +110,21 @@ module Aikido::Zen
109
110
  private
110
111
 
111
112
  def match?(conn_uri, input_uri)
112
- return false if conn_uri.hostname.nil? || conn_uri.hostname.empty?
113
+ # If a URI does not include a scheme then hostname and port are nil.
114
+ # No further comparison is necessary, because the conn_uri hostname
115
+ # and port are presumed to be non-nil.
113
116
  return false if input_uri.hostname.nil? || input_uri.hostname.empty?
114
117
 
115
- # The URI library will automatically set the port to the default port
116
- # for the current scheme if not provided, which means we can't just
117
- # check if the port is present, as it always will be.
118
+ # If a URI does not include a port then port is the default port
119
+ # for the scheme, otherwise, port is nil. If the input_uri port is
120
+ # the default port for the scheme then port comparison is skipped,
121
+ # because the user input is presumed not to have included a port.
118
122
  is_port_relevant = input_uri.port != input_uri.default_port
119
- return false if is_port_relevant && input_uri.port != conn_uri.port
123
+ return false if is_port_relevant && conn_uri.port != input_uri.port
120
124
 
121
- conn_uri.hostname == input_uri.hostname &&
122
- conn_uri.port == input_uri.port
125
+ # The port is either not relevant or is relevant and the conn_uri
126
+ # port and the input_uri port match. Do not force port comparison!
127
+ conn_uri.hostname == input_uri.hostname
123
128
  end
124
129
 
125
130
  def private_ip?(hostname)
@@ -11,7 +11,7 @@ module Aikido::Zen
11
11
  false
12
12
  end
13
13
 
14
- def self.call(hostname:, addresses:, operation:, sink:, context:, **opts)
14
+ def self.call(hostname:, addresses:, operation:, scan:, sink:, context:, **opts)
15
15
  offending_address = new(hostname, addresses).attack?
16
16
  return if offending_address.nil?
17
17
 
@@ -94,13 +94,10 @@ module Aikido::Zen
94
94
  scanners.each do |scanner|
95
95
  next if scanner.skips_on_nil_context? && context.nil?
96
96
 
97
- result = scanner.call(sink: self, context: context, **scan_params)
97
+ result = scanner.call(scan: scan, sink: self, context: context, **scan_params)
98
98
  scans_performed += 1
99
99
 
100
100
  break result if result
101
- rescue Aikido::Zen::InternalsError => error
102
- Aikido::Zen.config.logger.warn(error.message)
103
- scan.track_error(error, scanner)
104
101
  rescue => error
105
102
  scan.track_error(error, scanner)
106
103
  end
@@ -21,7 +21,7 @@ require_relative "sinks/net_http"
21
21
 
22
22
  # http.rb aims to support and is tested against Ruby 3.0+:
23
23
  # https://github.com/httprb/http?tab=readme-ov-file#supported-ruby-versions
24
- require_relative "sinks/http" if RUBY_VERSION >= "3.0"
24
+ require_relative "sinks/http" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0")
25
25
 
26
26
  require_relative "sinks/httpx"
27
27
  require_relative "sinks/httpclient"
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Aikido
4
4
  module Zen
5
- VERSION = "1.3.0"
5
+ VERSION = "1.3.1"
6
6
 
7
7
  # The version of libzen_internals that we build against.
8
8
  LIBZEN_VERSION = "0.1.60"
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.3.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aikido Security
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-28 00:00:00.000000000 Z
11
+ date: 2026-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -87,6 +87,7 @@ files:
87
87
  - benchmarks/rails7.1_sql_injection.js
88
88
  - docs/banner.svg
89
89
  - docs/config.md
90
+ - docs/invalid-sql-queries.md
90
91
  - docs/proxy.md
91
92
  - docs/rails.md
92
93
  - docs/troubleshooting.md