aikido-zen 0.2.0-arm64-darwin → 1.0.1.beta.2-arm64-darwin
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/.aikido +6 -0
- data/.simplecov +6 -0
- data/README.md +67 -83
- data/benchmarks/README.md +8 -12
- data/docs/rails.md +1 -1
- data/lib/aikido/zen/agent.rb +10 -8
- data/lib/aikido/zen/api_client.rb +14 -4
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/collector.rb +12 -1
- data/lib/aikido/zen/config.rb +20 -0
- data/lib/aikido/zen/context.rb +4 -0
- data/lib/aikido/zen/detached_agent/agent.rb +78 -0
- data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
- data/lib/aikido/zen/detached_agent/server.rb +41 -0
- data/lib/aikido/zen/detached_agent.rb +2 -0
- data/lib/aikido/zen/errors.rb +8 -0
- data/lib/aikido/zen/internals.rb +41 -7
- data/lib/aikido/zen/libzen-v0.1.39-arm64-darwin.dylib +0 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +9 -3
- data/lib/aikido/zen/middleware/request_tracker.rb +6 -4
- data/lib/aikido/zen/outbound_connection_monitor.rb +4 -0
- data/lib/aikido/zen/rails_engine.rb +8 -8
- data/lib/aikido/zen/rate_limiter/breaker.rb +3 -3
- data/lib/aikido/zen/rate_limiter.rb +6 -11
- data/lib/aikido/zen/request/heuristic_router.rb +6 -0
- data/lib/aikido/zen/request/rails_router.rb +6 -18
- data/lib/aikido/zen/request/schema/auth_schemas.rb +14 -0
- data/lib/aikido/zen/request/schema.rb +18 -0
- data/lib/aikido/zen/runtime_settings.rb +2 -2
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +4 -2
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +4 -2
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +4 -2
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +33 -21
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +6 -1
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +6 -0
- data/lib/aikido/zen/sink.rb +11 -1
- data/lib/aikido/zen/sinks/action_controller.rb +9 -4
- data/lib/aikido/zen/sinks/async_http.rb +35 -16
- data/lib/aikido/zen/sinks/curb.rb +52 -26
- data/lib/aikido/zen/sinks/em_http.rb +39 -25
- data/lib/aikido/zen/sinks/excon.rb +63 -45
- data/lib/aikido/zen/sinks/file.rb +67 -71
- data/lib/aikido/zen/sinks/http.rb +38 -19
- data/lib/aikido/zen/sinks/httpclient.rb +51 -22
- data/lib/aikido/zen/sinks/httpx.rb +37 -18
- data/lib/aikido/zen/sinks/kernel.rb +18 -57
- data/lib/aikido/zen/sinks/mysql2.rb +19 -7
- data/lib/aikido/zen/sinks/net_http.rb +37 -19
- data/lib/aikido/zen/sinks/patron.rb +41 -24
- data/lib/aikido/zen/sinks/pg.rb +50 -27
- data/lib/aikido/zen/sinks/resolv.rb +37 -16
- data/lib/aikido/zen/sinks/socket.rb +46 -17
- data/lib/aikido/zen/sinks/sqlite3.rb +31 -12
- data/lib/aikido/zen/sinks/trilogy.rb +19 -7
- data/lib/aikido/zen/sinks.rb +29 -20
- data/lib/aikido/zen/sinks_dsl.rb +226 -0
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen/worker.rb +5 -0
- data/lib/aikido/zen.rb +59 -9
- data/placeholder/.gitignore +4 -0
- data/placeholder/README.md +11 -0
- data/placeholder/Rakefile +75 -0
- data/placeholder/lib/placeholder.rb.template +3 -0
- data/placeholder/placeholder.gemspec.template +20 -0
- data/tasklib/bench.rake +29 -6
- data/tasklib/libzen.rake +70 -66
- data/tasklib/wrk.rb +88 -0
- metadata +23 -13
- data/CHANGELOG.md +0 -25
- data/lib/aikido/zen/libzen-v0.1.37.aarch64.dylib +0 -0
- data/lib/aikido.rb +0 -3
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# dRB Front object that will work as a bridge communication between child & parent
|
4
|
+
# processes.
|
5
|
+
# Every method is called from the child but it runs in the parent process.
|
6
|
+
module Aikido::Zen::DetachedAgent
|
7
|
+
class FrontObject
|
8
|
+
def initialize(
|
9
|
+
config: Aikido::Zen.config,
|
10
|
+
collector: Aikido::Zen.collector,
|
11
|
+
runtime_settings: Aikido::Zen.runtime_settings,
|
12
|
+
rate_limiter: Aikido::Zen::RateLimiter.new
|
13
|
+
)
|
14
|
+
@config = config
|
15
|
+
@collector = collector
|
16
|
+
@rate_limiter = rate_limiter
|
17
|
+
@runtime_settings = runtime_settings
|
18
|
+
end
|
19
|
+
|
20
|
+
RequestKind = Struct.new(:route, :schema, :ip, :actor)
|
21
|
+
|
22
|
+
def send_heartbeat_to_parent_process(heartbeat)
|
23
|
+
@collector.push_heartbeat(heartbeat)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Method called by child processes to get an up-to-date version of the
|
27
|
+
# runtime_settings
|
28
|
+
def updated_settings
|
29
|
+
@runtime_settings
|
30
|
+
end
|
31
|
+
|
32
|
+
def calculate_rate_limits(route, ip, actor_hash)
|
33
|
+
actor = Aikido::Zen::Actor(actor_hash) if actor_hash
|
34
|
+
@rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen::DetachedAgent
|
4
|
+
class Server
|
5
|
+
def initialize(config: Aikido::Zen.config)
|
6
|
+
@detached_agent_front = FrontObject.new
|
7
|
+
@drb_server = DRb.start_service(config.detached_agent_socket_path, @detached_agent_front)
|
8
|
+
|
9
|
+
# We don't want to see drb logs unless in debug mode
|
10
|
+
@drb_server.verbose = config.logger.debug?
|
11
|
+
end
|
12
|
+
|
13
|
+
def alive?
|
14
|
+
@drb_server.alive?
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop!
|
18
|
+
@drb_server.stop_service
|
19
|
+
DRb.stop_service
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def start!
|
24
|
+
Aikido::Zen.config.logger.debug("Starting DRb Server...")
|
25
|
+
max_attempts = 10
|
26
|
+
@server = new
|
27
|
+
|
28
|
+
attempts = 0
|
29
|
+
until @server.alive?
|
30
|
+
Aikido::Zen.config.logger.info("DRb Server still not alive. #{max_attempts - attempts} attempts remaining")
|
31
|
+
sleep 0.1
|
32
|
+
attempts += 1
|
33
|
+
raise Aikido::Zen::DetachedAgentError.new("Impossible to start the dRB server (socket=#{Aikido::Zen.config.detached_agent_socket_path})") \
|
34
|
+
if attempts == max_attempts
|
35
|
+
end
|
36
|
+
|
37
|
+
@server
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/aikido/zen/errors.rb
CHANGED
data/lib/aikido/zen/internals.rb
CHANGED
@@ -13,14 +13,48 @@ module Aikido::Zen
|
|
13
13
|
attr_accessor :libzen_name
|
14
14
|
end
|
15
15
|
|
16
|
-
self.
|
17
|
-
"libzen-v#{LIBZEN_VERSION}"
|
18
|
-
FFI::Platform::
|
19
|
-
|
20
|
-
|
16
|
+
def self.libzen_names
|
17
|
+
lib_name = "libzen-v#{LIBZEN_VERSION}"
|
18
|
+
lib_ext = FFI::Platform::LIBSUFFIX
|
19
|
+
|
20
|
+
# Gem::Platform#version should be understood as an arbitrary Ruby defined
|
21
|
+
# OS specific string. A platform with a version string is considered more
|
22
|
+
# specific than a platform without a version string.
|
23
|
+
# https://docs.ruby-lang.org/en/3.3/Gem/Platform.html
|
24
|
+
|
25
|
+
platform = Gem::Platform.local.dup
|
26
|
+
|
27
|
+
# Library names in preferred order.
|
28
|
+
#
|
29
|
+
# If two library names are added, the specific platform library names is
|
30
|
+
# first and the generic platform library name is second.
|
31
|
+
names = []
|
32
|
+
|
33
|
+
names << "#{lib_name}-#{platform}.#{lib_ext}"
|
34
|
+
|
35
|
+
unless platform.version.nil?
|
36
|
+
platform.version = nil
|
37
|
+
names << "#{lib_name}-#{platform}.#{lib_ext}"
|
38
|
+
end
|
39
|
+
|
40
|
+
names
|
41
|
+
end
|
42
|
+
|
43
|
+
# Load the most specific library
|
44
|
+
def self.load_libzen
|
45
|
+
libzen_names.each do |libzen_name|
|
46
|
+
libzen_path = File.expand_path(libzen_name, __dir__)
|
47
|
+
begin
|
48
|
+
return ffi_lib(libzen_path)
|
49
|
+
rescue LoadError
|
50
|
+
# empty
|
51
|
+
end
|
52
|
+
end
|
53
|
+
raise LoadError, "Could not load libzen"
|
54
|
+
end
|
21
55
|
|
22
56
|
begin
|
23
|
-
|
57
|
+
load_libzen
|
24
58
|
|
25
59
|
# @!method self.detect_sql_injection_native(query, input, dialect)
|
26
60
|
# @param (see .detect_sql_injection)
|
@@ -30,7 +64,7 @@ module Aikido::Zen
|
|
30
64
|
# calling libzen.
|
31
65
|
attach_function :detect_sql_injection_native, :detect_sql_injection,
|
32
66
|
[:string, :string, :int], :int
|
33
|
-
rescue LoadError, FFI::NotFoundError => err
|
67
|
+
rescue LoadError, FFI::NotFoundError => err # rubocop:disable Lint/ShadowedException
|
34
68
|
# :nocov:
|
35
69
|
|
36
70
|
# Emit an $stderr warning at startup.
|
Binary file
|
@@ -12,12 +12,12 @@ module Aikido::Zen
|
|
12
12
|
app,
|
13
13
|
config: Aikido::Zen.config,
|
14
14
|
settings: Aikido::Zen.runtime_settings,
|
15
|
-
|
15
|
+
detached_agent: Aikido::Zen.detached_agent
|
16
16
|
)
|
17
17
|
@app = app
|
18
18
|
@config = config
|
19
19
|
@settings = settings
|
20
|
-
@
|
20
|
+
@detached_agent = detached_agent
|
21
21
|
end
|
22
22
|
|
23
23
|
def call(env)
|
@@ -33,9 +33,15 @@ module Aikido::Zen
|
|
33
33
|
private
|
34
34
|
|
35
35
|
def should_throttle?(request)
|
36
|
+
return false unless @settings.endpoints[request.route].rate_limiting.enabled?
|
36
37
|
return false if @settings.skip_protection_for_ips.include?(request.ip)
|
37
38
|
|
38
|
-
@
|
39
|
+
result = @detached_agent.calculate_rate_limits(request)
|
40
|
+
|
41
|
+
return false unless result
|
42
|
+
|
43
|
+
request.env["aikido.rate_limiting"] = result
|
44
|
+
request.env["aikido.rate_limiting"].throttled?
|
39
45
|
end
|
40
46
|
end
|
41
47
|
end
|
@@ -13,14 +13,16 @@ module Aikido::Zen
|
|
13
13
|
request = Aikido::Zen::Middleware.request_from(env)
|
14
14
|
response = @app.call(env)
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
if Aikido::Zen.config.collect_api_schema? && request.route && track?(
|
16
|
+
if request.route && track?(
|
19
17
|
status_code: response[0],
|
20
18
|
route: request.route.path,
|
21
19
|
http_method: request.request_method
|
22
20
|
)
|
23
|
-
Aikido::Zen.
|
21
|
+
Aikido::Zen.track_request request
|
22
|
+
|
23
|
+
if Aikido::Zen.config.collect_api_schema?
|
24
|
+
Aikido::Zen.track_discovered_route(request)
|
25
|
+
end
|
24
26
|
end
|
25
27
|
|
26
28
|
response
|
@@ -5,6 +5,10 @@ module Aikido::Zen
|
|
5
5
|
# any Sink that wraps an HTTP library, and lets us keep track of any hosts to
|
6
6
|
# which the app communicates over HTTP.
|
7
7
|
module OutboundConnectionMonitor
|
8
|
+
def self.skips_on_nil_context?
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
8
12
|
# This simply reports the connection to the Agent, and always returns +nil+
|
9
13
|
# as it's not scanning for any particular attack.
|
10
14
|
#
|
@@ -10,7 +10,7 @@ module Aikido::Zen
|
|
10
10
|
end
|
11
11
|
|
12
12
|
initializer "aikido.add_middleware" do |app|
|
13
|
-
next
|
13
|
+
next unless config.zen.protect?
|
14
14
|
|
15
15
|
app.middleware.use Aikido::Zen::Middleware::SetContext
|
16
16
|
app.middleware.use Aikido::Zen::Middleware::CheckAllowedAddresses
|
@@ -33,9 +33,9 @@ module Aikido::Zen
|
|
33
33
|
initializer "aikido.configuration" do |app|
|
34
34
|
# Allow the logger to be configured before checking if disabled? so we can
|
35
35
|
# let the user know that the agent is disabled.
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
logger = ::Rails.logger
|
37
|
+
logger = ActiveSupport::TaggedLogging.new(logger) unless logger.respond_to?(:tagged)
|
38
|
+
app.config.zen.logger = logger.tagged("aikido")
|
39
39
|
|
40
40
|
app.config.zen.request_builder = Aikido::Zen::Context::RAILS_REQUEST_BUILDER
|
41
41
|
|
@@ -51,16 +51,16 @@ module Aikido::Zen
|
|
51
51
|
end
|
52
52
|
|
53
53
|
config.after_initialize do
|
54
|
-
|
55
|
-
config.zen.logger.warn("Zen has been disabled and will not run.")
|
56
|
-
next
|
57
|
-
end
|
54
|
+
next unless config.zen.protect?
|
58
55
|
|
59
56
|
# Make sure this is run at the end of the initialization process, so
|
60
57
|
# that any gems required after aikido-zen are detected and patched
|
61
58
|
# accordingly.
|
62
59
|
Aikido::Zen.load_sinks!
|
63
60
|
|
61
|
+
# It's important we start after loading sinks, so we can report the installed packages
|
62
|
+
Aikido::Zen.start!
|
63
|
+
|
64
64
|
# Agent's bootstrap process has finished —Controllers are patched to block
|
65
65
|
# unwanted requests, sinks are loaded, scanners are running—, so we mark
|
66
66
|
# the agent as installed.
|
@@ -31,13 +31,13 @@ module Aikido::Zen
|
|
31
31
|
@opened_at = @clock.call
|
32
32
|
end
|
33
33
|
|
34
|
-
# @param
|
34
|
+
# @param event_type [String] an event type which we'll use to decide
|
35
35
|
# if we should throttle it.
|
36
36
|
# @return [Boolean]
|
37
|
-
def throttle?(
|
37
|
+
def throttle?(event_type)
|
38
38
|
return true if open? && !try_close
|
39
39
|
|
40
|
-
result = @bucket.increment(
|
40
|
+
result = @bucket.increment(event_type)
|
41
41
|
result.throttled?
|
42
42
|
end
|
43
43
|
|
@@ -24,23 +24,18 @@ module Aikido::Zen
|
|
24
24
|
}
|
25
25
|
end
|
26
26
|
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# the result of the check, and including useful stats in case you want to
|
30
|
-
# return RateLimit headers..
|
27
|
+
# Calculate based on the configuration whether a request will be
|
28
|
+
# rate-limited or not.
|
31
29
|
#
|
32
30
|
# @param request [Aikido::Zen::Request]
|
33
|
-
# @return [
|
34
|
-
|
35
|
-
# @see Aikido::Zen::RateLimiter::Result
|
36
|
-
def throttle?(request)
|
31
|
+
# @return [Aikido::Zen::RateLimiter::Result, nil]
|
32
|
+
def calculate_rate_limits(request)
|
37
33
|
settings = settings_for(request.route)
|
38
|
-
return
|
34
|
+
return nil unless settings.enabled?
|
39
35
|
|
40
36
|
bucket = @buckets[request.route]
|
41
37
|
key = @config.rate_limiting_discriminator.call(request)
|
42
|
-
|
43
|
-
request.env["aikido.rate_limiting"].throttled?
|
38
|
+
bucket.increment(key)
|
44
39
|
end
|
45
40
|
|
46
41
|
private
|
@@ -30,6 +30,10 @@ module Aikido::Zen
|
|
30
30
|
|
31
31
|
private def parameterize_segment(segment)
|
32
32
|
case segment
|
33
|
+
when ULID
|
34
|
+
":ulid"
|
35
|
+
when OBJECT_ID
|
36
|
+
":objectId"
|
33
37
|
when NUMBER
|
34
38
|
":number"
|
35
39
|
when UUID
|
@@ -57,6 +61,8 @@ module Aikido::Zen
|
|
57
61
|
| 00000000-0000-0000-0000-000000000000
|
58
62
|
| ffffffff-ffff-ffff-ffff-ffffffffffff
|
59
63
|
)\z/ix
|
64
|
+
ULID = /\A[0-9A-HJKMNP-TV-Z]{26}\z/i
|
65
|
+
OBJECT_ID = /\A[0-9a-f]{24}\z/i
|
60
66
|
EMAIL = /\A
|
61
67
|
[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+
|
62
68
|
@
|
@@ -58,27 +58,15 @@ module Aikido::Zen
|
|
58
58
|
end
|
59
59
|
|
60
60
|
private def build_route(route, request, prefix: request.script_name)
|
61
|
-
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
module Rails
|
66
|
-
class Route < Aikido::Zen::Route
|
67
|
-
attr_reader :verb
|
61
|
+
route_wrapper = ActionDispatch::Routing::RouteWrapper.new(route)
|
68
62
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
63
|
+
path = if prefix.present?
|
64
|
+
File.join(prefix.to_s, route_wrapper.path).chomp("/")
|
65
|
+
else
|
66
|
+
route_wrapper.path
|
73
67
|
end
|
74
68
|
|
75
|
-
|
76
|
-
if @prefix.present?
|
77
|
-
File.join(@prefix.to_s, @route.path).chomp("/")
|
78
|
-
else
|
79
|
-
@route.path
|
80
|
-
end
|
81
|
-
end
|
69
|
+
Aikido::Zen::Route.new(verb: request.request_method, path: path)
|
82
70
|
end
|
83
71
|
end
|
84
72
|
end
|
@@ -18,6 +18,20 @@ module Aikido::Zen
|
|
18
18
|
@schemas.map(&:as_json) unless @schemas.empty?
|
19
19
|
end
|
20
20
|
|
21
|
+
def self.from_json(schemas_array)
|
22
|
+
return NONE if !schemas_array || schemas_array.empty?
|
23
|
+
|
24
|
+
AuthSchemas.new(schemas_array.map do |schema|
|
25
|
+
if schema[:type] == "http"
|
26
|
+
Authorization.new(schema[:scheme])
|
27
|
+
elsif schema[:type] == "apiKey"
|
28
|
+
ApiKey.new(schema[:in], schema[:name])
|
29
|
+
else
|
30
|
+
raise "Invalid schema type: #{schema[:type]}"
|
31
|
+
end
|
32
|
+
end)
|
33
|
+
end
|
34
|
+
|
21
35
|
def ==(other)
|
22
36
|
other.is_a?(self.class) && schemas == other.schemas
|
23
37
|
end
|
@@ -48,6 +48,24 @@ module Aikido::Zen
|
|
48
48
|
{body: body, query: query_schema.as_json, auth: auth_schema.as_json}.compact
|
49
49
|
end
|
50
50
|
|
51
|
+
def self.from_json(data)
|
52
|
+
if data.empty?
|
53
|
+
return Request::Schema.new(
|
54
|
+
content_type: nil,
|
55
|
+
body_schema: EMPTY_SCHEMA,
|
56
|
+
query_schema: EMPTY_SCHEMA,
|
57
|
+
auth_schema: Aikido::Zen::Request::Schema::AuthSchemas.new([])
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
Request::Schema.new(
|
62
|
+
content_type: data[:body].nil? ? nil : data[:body][:type],
|
63
|
+
body_schema: data[:body].nil? ? EMPTY_SCHEMA : Aikido::Zen::Request::Schema::Definition.new(data[:body][:schema]),
|
64
|
+
query_schema: data[:query].nil? ? EMPTY_SCHEMA : Aikido::Zen::Request::Schema::Definition.new(data[:query]),
|
65
|
+
auth_schema: Aikido::Zen::Request::Schema::AuthSchemas.from_json(data[:auth])
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
51
69
|
# Merges the request specification with another request's specification.
|
52
70
|
#
|
53
71
|
# @param other [Aikido::Zen::Request::Schema, nil]
|
@@ -45,7 +45,7 @@ module Aikido::Zen
|
|
45
45
|
# @param data [Hash] the decoded JSON payload from the /api/runtime/config
|
46
46
|
# API endpoint.
|
47
47
|
#
|
48
|
-
# @return [
|
48
|
+
# @return [bool]
|
49
49
|
def update_from_json(data)
|
50
50
|
last_updated_at = updated_at
|
51
51
|
|
@@ -56,7 +56,7 @@ module Aikido::Zen
|
|
56
56
|
self.skip_protection_for_ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
|
57
57
|
self.received_any_stats = data["receivedAnyStats"]
|
58
58
|
|
59
|
-
|
59
|
+
updated_at != last_updated_at
|
60
60
|
end
|
61
61
|
end
|
62
62
|
end
|
@@ -5,6 +5,10 @@ require_relative "path_traversal/helpers"
|
|
5
5
|
module Aikido::Zen
|
6
6
|
module Scanners
|
7
7
|
class PathTraversalScanner
|
8
|
+
def self.skips_on_nil_context?
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
8
12
|
# Checks if the user introduced input is trying to access other path using
|
9
13
|
# Path Traversal kind of attacks.
|
10
14
|
#
|
@@ -16,8 +20,6 @@ module Aikido::Zen
|
|
16
20
|
# @return [Aikido::Zen::Attacks::PathTraversalAttack, nil] an Attack if any
|
17
21
|
# user input is detected to be attempting a Path Traversal Attack, or +nil+ if not.
|
18
22
|
def self.call(filepath:, sink:, context:, operation:)
|
19
|
-
return unless context
|
20
|
-
|
21
23
|
context.payloads.each do |payload|
|
22
24
|
next unless new(filepath, payload.value).attack?
|
23
25
|
|
@@ -5,14 +5,16 @@ require_relative "shell_injection/helpers"
|
|
5
5
|
module Aikido::Zen
|
6
6
|
module Scanners
|
7
7
|
class ShellInjectionScanner
|
8
|
+
def self.skips_on_nil_context?
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
8
12
|
# @param command [String]
|
9
13
|
# @param sink [Aikido::Zen::Sink]
|
10
14
|
# @param context [Aikido::Zen::Context]
|
11
15
|
# @param operation [Symbol, String]
|
12
16
|
#
|
13
17
|
def self.call(command:, sink:, context:, operation:)
|
14
|
-
return unless context
|
15
|
-
|
16
18
|
context.payloads.each do |payload|
|
17
19
|
next unless new(command, payload.value).attack?
|
18
20
|
|
@@ -6,6 +6,10 @@ require_relative "../internals"
|
|
6
6
|
module Aikido::Zen
|
7
7
|
module Scanners
|
8
8
|
class SQLInjectionScanner
|
9
|
+
def self.skips_on_nil_context?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
9
13
|
# Checks if the given SQL query may have dangerous user input injected,
|
10
14
|
# and returns an Attack if so, based on the current request.
|
11
15
|
#
|
@@ -22,8 +26,6 @@ module Aikido::Zen
|
|
22
26
|
# @raise [Aikido::Zen::InternalsError] if an error occurs when loading or
|
23
27
|
# calling zenlib. See Sink#scan.
|
24
28
|
def self.call(query:, dialect:, sink:, context:, operation:)
|
25
|
-
return if context.nil?
|
26
|
-
|
27
29
|
dialect = DIALECTS.fetch(dialect) do
|
28
30
|
Aikido::Zen.config.logger.warn "Unknown SQL dialect #{dialect.inspect}"
|
29
31
|
DIALECTS[:common]
|
@@ -31,35 +31,47 @@ module Aikido::Zen
|
|
31
31
|
# @return [Boolean]
|
32
32
|
def private?(hostname_or_address)
|
33
33
|
resolve(hostname_or_address).any? do |ip|
|
34
|
-
|
34
|
+
PRIVATE_RANGES.any? { |range| range === ip }
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
38
|
private
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
IPAddr.new("
|
43
|
-
IPAddr.new("
|
44
|
-
IPAddr.new("
|
45
|
-
IPAddr.new("
|
46
|
-
IPAddr.new("
|
47
|
-
IPAddr.new("
|
48
|
-
IPAddr.new("192.
|
49
|
-
IPAddr.new("192.
|
50
|
-
IPAddr.new("192.
|
51
|
-
IPAddr.new("
|
52
|
-
IPAddr.new("
|
53
|
-
IPAddr.new("
|
54
|
-
IPAddr.new("
|
55
|
-
IPAddr.new("
|
56
|
-
IPAddr.new("
|
40
|
+
# Source: https://github.com/AikidoSec/firewall-node/blob/main/library/vulnerabilities/ssrf/isPrivateIP.ts
|
41
|
+
PRIVATE_IPV4_RANGES = [
|
42
|
+
IPAddr.new("0.0.0.0/8"), # "This" network (RFC 1122)
|
43
|
+
IPAddr.new("10.0.0.0/8"), # Private-Use Networks (RFC 1918)
|
44
|
+
IPAddr.new("100.64.0.0/10"), # Shared Address Space (RFC 6598)
|
45
|
+
IPAddr.new("127.0.0.0/8"), # Loopback (RFC 1122)
|
46
|
+
IPAddr.new("169.254.0.0/16"), # Link Local (RFC 3927)
|
47
|
+
IPAddr.new("172.16.0.0/12"), # Private-Use Networks (RFC 1918)
|
48
|
+
IPAddr.new("192.0.0.0/24"), # IETF Protocol Assignments (RFC 5736)
|
49
|
+
IPAddr.new("192.0.2.0/24"), # TEST-NET-1 (RFC 5737)
|
50
|
+
IPAddr.new("192.31.196.0/24"), # AS112 Redirection Anycast (RFC 7535)
|
51
|
+
IPAddr.new("192.52.193.0/24"), # Automatic Multicast Tunneling (RFC 7450)
|
52
|
+
IPAddr.new("192.88.99.0/24"), # 6to4 Relay Anycast (RFC 3068)
|
53
|
+
IPAddr.new("192.168.0.0/16"), # Private-Use Networks (RFC 1918)
|
54
|
+
IPAddr.new("192.175.48.0/24"), # AS112 Redirection Anycast (RFC 7535)
|
55
|
+
IPAddr.new("198.18.0.0/15"), # Network Interconnect Device Benchmark Testing (RFC 2544)
|
56
|
+
IPAddr.new("198.51.100.0/24"), # TEST-NET-2 (RFC 5737)
|
57
|
+
IPAddr.new("203.0.113.0/24"), # TEST-NET-3 (RFC 5737)
|
58
|
+
IPAddr.new("224.0.0.0/4"), # Multicast (RFC 3171)
|
59
|
+
IPAddr.new("240.0.0.0/4"), # Reserved for Future Use (RFC 1112)
|
60
|
+
IPAddr.new("255.255.255.255/32") # Limited Broadcast (RFC 919)
|
61
|
+
]
|
57
62
|
|
58
|
-
|
59
|
-
IPAddr.new("
|
60
|
-
IPAddr.new("::
|
63
|
+
PRIVATE_IPV6_RANGES = [
|
64
|
+
IPAddr.new("::/128"), # Unspecified address (RFC 4291)
|
65
|
+
IPAddr.new("::1/128"), # Loopback address (RFC 4291)
|
66
|
+
IPAddr.new("fc00::/7"), # Unique local address (ULA) (RFC 4193
|
67
|
+
IPAddr.new("fe80::/10"), # Link-local address (LLA) (RFC 4291)
|
68
|
+
IPAddr.new("100::/64"), # Discard prefix (RFC 6666)
|
69
|
+
IPAddr.new("2001:db8::/32"), # Documentation prefix (RFC 3849)
|
70
|
+
IPAddr.new("3fff::/20") # Documentation prefix (RFC 9637)
|
61
71
|
]
|
62
72
|
|
73
|
+
PRIVATE_RANGES = PRIVATE_IPV4_RANGES + PRIVATE_IPV6_RANGES + PRIVATE_IPV4_RANGES.map(&:ipv4_mapped)
|
74
|
+
|
63
75
|
def resolved_in_current_context
|
64
76
|
context = Aikido::Zen.current_context
|
65
77
|
context && context["dns.lookups"]
|
@@ -6,6 +6,12 @@ require_relative "ssrf/dns_lookups"
|
|
6
6
|
module Aikido::Zen
|
7
7
|
module Scanners
|
8
8
|
class SSRFScanner
|
9
|
+
# SSRF attacks can be triggered through external inputs, so it is essential
|
10
|
+
# to have a valid context to safeguard against these attacks.
|
11
|
+
def self.skips_on_nil_context?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
9
15
|
# Checks if an outbound HTTP request is to a hostname supplied from user
|
10
16
|
# input that resolves to a "dangerous" address. This is called from two
|
11
17
|
# different places:
|
@@ -32,7 +38,6 @@ module Aikido::Zen
|
|
32
38
|
# @return [Aikido::Zen::Attacks::SSRFAttack, nil] an Attack if any user
|
33
39
|
# input is detected to be attempting SSRF, or +nil+ if not.
|
34
40
|
def self.call(request:, sink:, context:, operation:, **)
|
35
|
-
return if context.nil?
|
36
41
|
return if request.nil? # See NOTE above.
|
37
42
|
|
38
43
|
context["ssrf.redirects"] ||= RedirectChains.new
|
@@ -5,6 +5,12 @@ module Aikido::Zen
|
|
5
5
|
# Inspects the result of DNS lookups, to determine if we're being the target
|
6
6
|
# of a stored SSRF targeting IMDS addresses (169.254.169.254).
|
7
7
|
class StoredSSRFScanner
|
8
|
+
# Stored-SSRF can occur without external input, so we do not require a
|
9
|
+
# context to determine if an attack is happening.
|
10
|
+
def self.skips_on_nil_context?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
8
14
|
def self.call(hostname:, addresses:, operation:, sink:, context:, **opts)
|
9
15
|
offending_address = new(hostname, addresses).attack?
|
10
16
|
return if offending_address.nil?
|
data/lib/aikido/zen/sink.rb
CHANGED
@@ -80,15 +80,23 @@ module Aikido::Zen
|
|
80
80
|
# @raise [Aikido::UnderAttackError] if an attack is detected and
|
81
81
|
# blocking_mode is enabled.
|
82
82
|
def scan(context: Aikido::Zen.current_context, **scan_params)
|
83
|
+
return if context&.scanning
|
84
|
+
context&.scanning = true
|
85
|
+
|
83
86
|
return if context&.protection_disabled?
|
84
87
|
|
85
88
|
scan = Scan.new(sink: self, context: context)
|
86
89
|
|
90
|
+
scans_performed = 0
|
87
91
|
scan.perform do
|
88
92
|
result = nil
|
89
93
|
|
90
94
|
scanners.each do |scanner|
|
95
|
+
next if scanner.skips_on_nil_context? && context.nil?
|
96
|
+
|
91
97
|
result = scanner.call(sink: self, context: context, **scan_params)
|
98
|
+
scans_performed += 1
|
99
|
+
|
92
100
|
break result if result
|
93
101
|
rescue Aikido::Zen::InternalsError => error
|
94
102
|
Aikido::Zen.config.logger.warn(error.message)
|
@@ -100,9 +108,11 @@ module Aikido::Zen
|
|
100
108
|
result
|
101
109
|
end
|
102
110
|
|
103
|
-
@reporter.call(scan)
|
111
|
+
@reporter.call(scan) if scans_performed > 0
|
104
112
|
|
105
113
|
scan
|
114
|
+
ensure
|
115
|
+
context&.scanning = false
|
106
116
|
end
|
107
117
|
end
|
108
118
|
end
|