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 +4 -4
- data/.simplecov +1 -1
- data/docs/invalid-sql-queries.md +9 -0
- data/lib/aikido/zen/attack.rb +5 -3
- data/lib/aikido/zen/config.rb +28 -15
- data/lib/aikido/zen/context/rack_request.rb +28 -4
- data/lib/aikido/zen/context/rails_request.rb +42 -6
- data/lib/aikido/zen/helpers.rb +10 -0
- data/lib/aikido/zen/internals.rb +5 -7
- data/lib/aikido/zen/request.rb +9 -5
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +5 -2
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +4 -1
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +32 -11
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +17 -12
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +1 -1
- data/lib/aikido/zen/sink.rb +1 -4
- data/lib/aikido/zen/sinks.rb +1 -1
- data/lib/aikido/zen/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfcc07a5e3256e9d9d35f89bea1cf884d2c51bf23d497a40401f61d7335a3dbc
|
|
4
|
+
data.tar.gz: 81055a97f6a640bafb3bfd29fbc96f0c0eeb165046191bd68518109908d92817
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`.
|
data/lib/aikido/zen/attack.rb
CHANGED
|
@@ -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(*)
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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:
|
|
19
|
-
body:
|
|
42
|
+
query: query,
|
|
43
|
+
body: body,
|
|
20
44
|
route: {},
|
|
21
|
-
header:
|
|
22
|
-
cookie:
|
|
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:
|
|
39
|
-
body:
|
|
40
|
-
route:
|
|
41
|
-
header:
|
|
42
|
-
cookie:
|
|
43
|
-
subdomain:
|
|
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
|
data/lib/aikido/zen/helpers.rb
CHANGED
|
@@ -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
|
data/lib/aikido/zen/internals.rb
CHANGED
|
@@ -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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
data/lib/aikido/zen/request.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Aikido::Zen
|
|
|
26
26
|
|
|
27
27
|
def __setobj__(delegate) # :nodoc:
|
|
28
28
|
super
|
|
29
|
-
@route = @
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
116
|
-
# for the
|
|
117
|
-
#
|
|
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 &&
|
|
123
|
+
return false if is_port_relevant && conn_uri.port != input_uri.port
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
data/lib/aikido/zen/sink.rb
CHANGED
|
@@ -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
|
data/lib/aikido/zen/sinks.rb
CHANGED
|
@@ -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"
|
data/lib/aikido/zen/version.rb
CHANGED
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.
|
|
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-
|
|
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
|