aikido-zen 0.1.0.alpha4-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 +7 -0
- data/.ruby-version +1 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +674 -0
- data/README.md +40 -0
- data/Rakefile +63 -0
- data/lib/aikido/zen/actor.rb +116 -0
- data/lib/aikido/zen/agent.rb +187 -0
- data/lib/aikido/zen/api_client.rb +132 -0
- data/lib/aikido/zen/attack.rb +138 -0
- data/lib/aikido/zen/capped_collections.rb +68 -0
- data/lib/aikido/zen/config.rb +229 -0
- data/lib/aikido/zen/context/rack_request.rb +24 -0
- data/lib/aikido/zen/context/rails_request.rb +42 -0
- data/lib/aikido/zen/context.rb +101 -0
- data/lib/aikido/zen/errors.rb +88 -0
- data/lib/aikido/zen/event.rb +66 -0
- data/lib/aikido/zen/internals.rb +64 -0
- data/lib/aikido/zen/libzen-v0.1.26.aarch64.dylib +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
- data/lib/aikido/zen/middleware/set_context.rb +26 -0
- data/lib/aikido/zen/middleware/throttler.rb +50 -0
- data/lib/aikido/zen/outbound_connection.rb +45 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +48 -0
- data/lib/aikido/zen/rails_engine.rb +53 -0
- data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
- data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
- data/lib/aikido/zen/rate_limiter/result.rb +31 -0
- data/lib/aikido/zen/rate_limiter.rb +55 -0
- data/lib/aikido/zen/request/heuristic_router.rb +109 -0
- data/lib/aikido/zen/request/rails_router.rb +84 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
- data/lib/aikido/zen/request/schema/builder.rb +125 -0
- data/lib/aikido/zen/request/schema/definition.rb +112 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +72 -0
- data/lib/aikido/zen/request.rb +97 -0
- data/lib/aikido/zen/route.rb +39 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
- data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
- data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
- data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
- data/lib/aikido/zen/runtime_settings.rb +70 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
- data/lib/aikido/zen/scanners.rb +5 -0
- data/lib/aikido/zen/sink.rb +108 -0
- data/lib/aikido/zen/sinks/async_http.rb +63 -0
- data/lib/aikido/zen/sinks/curb.rb +89 -0
- data/lib/aikido/zen/sinks/em_http.rb +71 -0
- data/lib/aikido/zen/sinks/excon.rb +103 -0
- data/lib/aikido/zen/sinks/http.rb +76 -0
- data/lib/aikido/zen/sinks/httpclient.rb +68 -0
- data/lib/aikido/zen/sinks/httpx.rb +61 -0
- data/lib/aikido/zen/sinks/mysql2.rb +21 -0
- data/lib/aikido/zen/sinks/net_http.rb +85 -0
- data/lib/aikido/zen/sinks/patron.rb +88 -0
- data/lib/aikido/zen/sinks/pg.rb +50 -0
- data/lib/aikido/zen/sinks/resolv.rb +41 -0
- data/lib/aikido/zen/sinks/socket.rb +51 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
- data/lib/aikido/zen/sinks/trilogy.rb +21 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +21 -0
- data/lib/aikido/zen/stats/routes.rb +53 -0
- data/lib/aikido/zen/stats/sink_stats.rb +95 -0
- data/lib/aikido/zen/stats/users.rb +26 -0
- data/lib/aikido/zen/stats.rb +171 -0
- data/lib/aikido/zen/synchronizable.rb +24 -0
- data/lib/aikido/zen/system_info.rb +84 -0
- data/lib/aikido/zen/version.rb +10 -0
- data/lib/aikido/zen.rb +138 -0
- data/lib/aikido-zen.rb +3 -0
- data/lib/aikido.rb +3 -0
- data/tasklib/libzen.rake +128 -0
- metadata +175 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../route"
|
4
|
+
require_relative "protection_settings"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
# Wraps the list of endpoint protection settings, providing an interface for
|
8
|
+
# checking the settings for any given route. If the route has no configured
|
9
|
+
# settings, that will return the singleton
|
10
|
+
# {RuntimeSettings::ProtectionSettings.none}.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# endpoint = runtime_settings.endpoints[request.route]
|
14
|
+
# block_request unless endpoint.allows?(request.ip)
|
15
|
+
class RuntimeSettings::Endpoints
|
16
|
+
# @param data [Array<Hash>]
|
17
|
+
# @return [Aikido::Zen::RuntimeSettings::Endpoints]
|
18
|
+
def self.from_json(data)
|
19
|
+
data = Array(data).map { |item|
|
20
|
+
route = Route.new(verb: item["method"], path: item["route"])
|
21
|
+
settings = RuntimeSettings::ProtectionSettings.from_json(item)
|
22
|
+
[route, settings]
|
23
|
+
}.to_h
|
24
|
+
|
25
|
+
new(data)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(data = {})
|
29
|
+
@endpoints = data
|
30
|
+
@endpoints.default = RuntimeSettings::ProtectionSettings.none
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param route [Aikido::Zen::Route]
|
34
|
+
# @return [Aikido::Zen::RuntimeSettings::ProtectionSettings]
|
35
|
+
def [](route)
|
36
|
+
@endpoints[route]
|
37
|
+
end
|
38
|
+
|
39
|
+
# @!visibility private
|
40
|
+
def ==(other)
|
41
|
+
other.is_a?(RuntimeSettings::Endpoints) && to_h == other.to_h
|
42
|
+
end
|
43
|
+
|
44
|
+
# @!visibility private
|
45
|
+
protected def to_h
|
46
|
+
@endpoints
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ipaddr"
|
4
|
+
|
5
|
+
module Aikido::Zen
|
6
|
+
# Models a list of IP addresses or CIDR blocks, where we can check if a given
|
7
|
+
# address is part of any of the members.
|
8
|
+
class RuntimeSettings::IPSet
|
9
|
+
def self.from_json(ips)
|
10
|
+
new(Array(ips).map { |ip| IPAddr.new(ip) })
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(ips = Set.new)
|
14
|
+
@ips = ips.to_set
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
@ips.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def include?(ip)
|
22
|
+
@ips.any? { |pattern| pattern === ip }
|
23
|
+
end
|
24
|
+
alias_method :===, :include?
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
other.is_a?(RuntimeSettings::IPSet) && to_set == other.to_set
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def to_set
|
33
|
+
@ips
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "ip_set"
|
4
|
+
require_relative "rate_limit_settings"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
# Models the settings for a given Route as configured in the Aikido UI.
|
8
|
+
class RuntimeSettings::ProtectionSettings
|
9
|
+
# @return [Aikido::Zen::RuntimeSettings::ProtectionSettings] singleton
|
10
|
+
# instance for endpoints with no configured protections on a given route,
|
11
|
+
# that can be used as a default value for routes.
|
12
|
+
def self.none
|
13
|
+
@no_settings ||= new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Initialize settings from an API response.
|
17
|
+
#
|
18
|
+
# @param data [Hash] the deserialized JSON data.
|
19
|
+
# @option data [Boolean] "forceProtectionOff" whether the user has
|
20
|
+
# disabled attack protection for this route.
|
21
|
+
# @option data [Array<String>] "allowedIPAddresses" the list of IPs that
|
22
|
+
# can make requests to this endpoint.
|
23
|
+
# @option data [Hash] "rateLimiting" the rate limiting options for this
|
24
|
+
# endpoint. See {Aikido::Zen::RuntimeSettings::RateLimitSettings.from_json}.
|
25
|
+
#
|
26
|
+
# @return [Aikido::Zen::RuntimeSettings::ProtectionSettings]
|
27
|
+
# @raise [IPAddr::InvalidAddressError] if any of the IPs in
|
28
|
+
# "allowedIPAddresses" is not a valid address or family.
|
29
|
+
def self.from_json(data)
|
30
|
+
ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
|
31
|
+
rate_limiting = RuntimeSettings::RateLimitSettings.from_json(data["rateLimiting"])
|
32
|
+
|
33
|
+
new(
|
34
|
+
protected: !data["forceProtectionOff"],
|
35
|
+
allowed_ips: ips,
|
36
|
+
rate_limiting: rate_limiting
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Aikido::Zen::RuntimeSettings::IPSet] list of IP addresses which
|
41
|
+
# are allowed to make requests on this route. If empty, all IP addresses
|
42
|
+
# are allowed.
|
43
|
+
attr_reader :allowed_ips
|
44
|
+
|
45
|
+
# @return [Aikido::Zen::RuntimeSettings::RateLimitSettings]
|
46
|
+
attr_reader :rate_limiting
|
47
|
+
|
48
|
+
def initialize(
|
49
|
+
protected: true,
|
50
|
+
allowed_ips: RuntimeSettings::IPSet.new,
|
51
|
+
rate_limiting: RuntimeSettings::RateLimitSettings.disabled
|
52
|
+
)
|
53
|
+
@protected = !!protected
|
54
|
+
@rate_limiting = rate_limiting
|
55
|
+
@allowed_ips = allowed_ips
|
56
|
+
end
|
57
|
+
|
58
|
+
def protected?
|
59
|
+
@protected
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
# Simple data object that holds the configuration for rate limiting a given
|
5
|
+
# endpoint.
|
6
|
+
class RuntimeSettings::RateLimitSettings
|
7
|
+
# Initialize the settings from an API response.
|
8
|
+
#
|
9
|
+
# @param data [Hash] the deserialized JSON data.
|
10
|
+
# @option data [Boolean] "enabled"
|
11
|
+
# @option data [Integer] "maxRequests"
|
12
|
+
# @option data [Integer] "windowSizeInMS"
|
13
|
+
#
|
14
|
+
# @return [Aikido::Zen::RateLimitSettings]
|
15
|
+
def self.from_json(data)
|
16
|
+
new(
|
17
|
+
enabled: !!data["enabled"],
|
18
|
+
max_requests: Integer(data["maxRequests"]),
|
19
|
+
period: Integer(data["windowSizeInMS"]) / 1000
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Initializes a disabled object that we can use as a default value for
|
24
|
+
# endpoints that have not configured rate limiting.
|
25
|
+
#
|
26
|
+
# @return [Aikido::Zen::RuntimeSettings::RateLimitSettings]
|
27
|
+
def self.disabled
|
28
|
+
new(enabled: false)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Integer] the fixed window to bucket requests in, in seconds.
|
32
|
+
attr_reader :period
|
33
|
+
|
34
|
+
# @return [Integer]
|
35
|
+
attr_reader :max_requests
|
36
|
+
|
37
|
+
def initialize(enabled: false, max_requests: 1000, period: 60)
|
38
|
+
@enabled = enabled
|
39
|
+
@period = period
|
40
|
+
@max_requests = max_requests
|
41
|
+
end
|
42
|
+
|
43
|
+
def enabled?
|
44
|
+
@enabled
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
# Stores the firewall configuration sourced from the Aikido dashboard. This
|
5
|
+
# object is updated by the Agent regularly.
|
6
|
+
#
|
7
|
+
# Because the RuntimeSettings object can be modified in runtime, it implements
|
8
|
+
# the {Observable} API, allowing you to subscribe to updates. These are
|
9
|
+
# triggered whenever #update_from_json makes a change (i.e. if the settings
|
10
|
+
# don't change, no update is triggered).
|
11
|
+
#
|
12
|
+
# You can subscribe to changes with +#add_observer(object, func_name)+, which
|
13
|
+
# will call the function passing the settings as an argument.
|
14
|
+
class RuntimeSettings < Concurrent::MutableStruct.new(
|
15
|
+
:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :skip_protection_for_ips, :received_any_stats
|
16
|
+
)
|
17
|
+
include Concurrent::Concern::Observable
|
18
|
+
|
19
|
+
def initialize(*)
|
20
|
+
self.observers = Concurrent::Collection::CopyOnWriteObserverSet.new
|
21
|
+
super
|
22
|
+
self.endpoints ||= Endpoints.new
|
23
|
+
self.skip_protection_for_ips ||= IPSet.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# @!attribute [rw] updated_at
|
27
|
+
# @return [Time] when these settings were updated in the Aikido dashboard.
|
28
|
+
|
29
|
+
# @!attribute [rw] heartbeat_interval
|
30
|
+
# @return [Integer] duration in seconds between heartbeat requests to the
|
31
|
+
# Aikido server.
|
32
|
+
|
33
|
+
# @!attribute [rw] received_any_stats
|
34
|
+
# @return [Boolean] whether the Aikido server has received any data from
|
35
|
+
# this application.
|
36
|
+
|
37
|
+
# @!attribute [rw] endpoints
|
38
|
+
# @return [Aikido::Zen::RuntimeSettings::Endpoints]
|
39
|
+
|
40
|
+
# @!attribute [rw] blocked_user_ids
|
41
|
+
# @return [Array]
|
42
|
+
|
43
|
+
# @!attribute [rw] skip_protection_for_ips
|
44
|
+
# @return [Aikido::Zen::RuntimeSettings::IPSet]
|
45
|
+
|
46
|
+
# Parse and interpret the JSON response from the core API with updated
|
47
|
+
# settings, and apply the changes. This will also notify any subscriber
|
48
|
+
# to updates
|
49
|
+
#
|
50
|
+
# @param data [Hash] the decoded JSON payload from the /api/runtime/config
|
51
|
+
# API endpoint.
|
52
|
+
#
|
53
|
+
# @return [void]
|
54
|
+
def update_from_json(data)
|
55
|
+
last_updated_at = updated_at
|
56
|
+
|
57
|
+
self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
|
58
|
+
self.heartbeat_interval = (data["heartbeatIntervalInMS"].to_i / 1000)
|
59
|
+
self.endpoints = Endpoints.from_json(data["endpoints"])
|
60
|
+
self.blocked_user_ids = data["blockedUserIds"]
|
61
|
+
self.skip_protection_for_ips = IPSet.from_json(data["allowedIPAddresses"])
|
62
|
+
self.received_any_stats = data["receivedAnyStats"]
|
63
|
+
|
64
|
+
observers.notify_observers(self) if updated_at != last_updated_at
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
require_relative "runtime_settings/ip_set"
|
70
|
+
require_relative "runtime_settings/endpoints"
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
# Scans track information about a single call made by one of our Sinks
|
5
|
+
# including whether it was detected as an attack or how long it took.
|
6
|
+
class Scan
|
7
|
+
# @return [Aikido::Zen::Sink] the originating Sink.
|
8
|
+
attr_reader :sink
|
9
|
+
|
10
|
+
# @return [Aikido::Zen::Context] the current Context, wrapping the HTTP
|
11
|
+
# request during which this scan was performed.
|
12
|
+
attr_reader :context
|
13
|
+
|
14
|
+
# @return [Aikido::Zen::Attack, nil] a detected Attack, or
|
15
|
+
# +nil+ if the scan was considered safe.
|
16
|
+
attr_reader :attack
|
17
|
+
|
18
|
+
# @return [Float, nil] duration in (fractional) seconds of the scan.
|
19
|
+
attr_reader :duration
|
20
|
+
|
21
|
+
# @return [Array<Hash>] list of captured exceptions while scanning.
|
22
|
+
attr_reader :errors
|
23
|
+
|
24
|
+
# @param sink [Aikido::Zen::Sink]
|
25
|
+
# @param context [Aikido::Zen::Context]
|
26
|
+
def initialize(sink:, context:)
|
27
|
+
@sink = sink
|
28
|
+
@context = context
|
29
|
+
@errors = []
|
30
|
+
@performed = false
|
31
|
+
end
|
32
|
+
|
33
|
+
def performed?
|
34
|
+
@performed
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Boolean] whether this scan detected an Attack.
|
38
|
+
def attack?
|
39
|
+
@attack != nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Boolean] whether any errors were caught by this Scan.
|
43
|
+
def errors?
|
44
|
+
@errors.any?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Runs a block of code, capturing its return value as the potential
|
48
|
+
# Attack object (or nil, if safe), and how long it took to run.
|
49
|
+
#
|
50
|
+
# @yieldreturn [Aikido::Zen::Attack, nil]
|
51
|
+
# @return [void]
|
52
|
+
def perform
|
53
|
+
@performed = true
|
54
|
+
started_at = monotonic_time
|
55
|
+
@attack = yield
|
56
|
+
ensure
|
57
|
+
@duration = monotonic_time - started_at
|
58
|
+
end
|
59
|
+
|
60
|
+
# Keep track of exceptions encountered during scanning.
|
61
|
+
#
|
62
|
+
# @param error [Exception]
|
63
|
+
# @param scanner [#call]
|
64
|
+
#
|
65
|
+
# @return [nil]
|
66
|
+
def track_error(error, scanner)
|
67
|
+
@errors << {error: error, scanner: scanner}
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
private def monotonic_time
|
72
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../attack"
|
4
|
+
require_relative "../internals"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
module Scanners
|
8
|
+
class SQLInjectionScanner
|
9
|
+
# Checks if the given SQL query may have dangerous user input injected,
|
10
|
+
# and returns an Attack if so, based on the current request.
|
11
|
+
#
|
12
|
+
# @param query [String]
|
13
|
+
# @param context [Aikido::Zen::Context]
|
14
|
+
# @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
|
15
|
+
# @param dialect [Symbol] one of +:mysql+, +:postgesql+, or +:sqlite+.
|
16
|
+
# @param operation [Symbol, String] name of the method being scanned.
|
17
|
+
# Expects +sink.operation+ being set to get the full module/name combo.
|
18
|
+
#
|
19
|
+
# @return [Aikido::Zen::Attack, nil] an Attack if any user input is
|
20
|
+
# detected to be attempting a SQL injection, or nil if this is safe.
|
21
|
+
#
|
22
|
+
# @raise [Aikido::Zen::InternalsError] if an error occurs when loading or
|
23
|
+
# calling zenlib. See Sink#scan.
|
24
|
+
def self.call(query:, dialect:, sink:, context:, operation:)
|
25
|
+
# FIXME: This assumes queries executed outside of an HTTP request are
|
26
|
+
# safe, but this is not the case. For example, if an HTTP request
|
27
|
+
# enqueues a background job, passing user input verbatim, the job might
|
28
|
+
# pass that input to a query without having a current request in scope.
|
29
|
+
return if context.nil?
|
30
|
+
|
31
|
+
dialect = DIALECTS.fetch(dialect) do
|
32
|
+
Aikido::Zen.config.logger.warn "Unknown SQL dialect #{dialect.inspect}"
|
33
|
+
DIALECTS[:common]
|
34
|
+
end
|
35
|
+
|
36
|
+
context.payloads.each do |payload|
|
37
|
+
next unless new(query, payload.value, dialect).attack?
|
38
|
+
|
39
|
+
return Attacks::SQLInjectionAttack.new(
|
40
|
+
sink: sink,
|
41
|
+
query: query,
|
42
|
+
input: payload,
|
43
|
+
dialect: dialect,
|
44
|
+
context: context,
|
45
|
+
operation: "#{sink.operation}.#{operation}"
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(query, input, dialect)
|
53
|
+
@query = query.downcase
|
54
|
+
@input = input.downcase
|
55
|
+
@dialect = dialect
|
56
|
+
end
|
57
|
+
|
58
|
+
def attack?
|
59
|
+
# Ignore single char inputs since they shouldn't be able to do much harm
|
60
|
+
return false if @input.length <= 1
|
61
|
+
|
62
|
+
# If the input is longer than the query, then it is not part of it
|
63
|
+
return false if @input.length > @query.length
|
64
|
+
|
65
|
+
# If the input is not included in the query at all, then we are safe
|
66
|
+
return false unless @query.include?(@input)
|
67
|
+
|
68
|
+
# If the input is solely alphanumeric, we can ignore it
|
69
|
+
return false if /\A[[:alnum:]_]+\z/i.match?(@input)
|
70
|
+
|
71
|
+
# If the input is a comma-separated list of numbers, ignore it.
|
72
|
+
return false if /\A(?:\d+(?:,\s*)?)+\z/i.match?(@input)
|
73
|
+
|
74
|
+
Internals.detect_sql_injection(@query, @input, @dialect)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @api private
|
78
|
+
Dialect = Struct.new(:name, :internals_key, keyword_init: true) do
|
79
|
+
alias_method :to_s, :name
|
80
|
+
alias_method :to_int, :internals_key
|
81
|
+
end
|
82
|
+
|
83
|
+
# Maps easy-to-use Symbols to a struct that keeps both the name and the
|
84
|
+
# internal identifier used by libzen.
|
85
|
+
#
|
86
|
+
# @see https://github.com/AikidoSec/zen-internals/blob/main/src/sql_injection/helpers/select_dialect_based_on_enum.rs
|
87
|
+
DIALECTS = {
|
88
|
+
common: Dialect.new(name: "SQL", internals_key: 0),
|
89
|
+
mysql: Dialect.new(name: "MySQL", internals_key: 8),
|
90
|
+
postgresql: Dialect.new(name: "PostgreSQL", internals_key: 9),
|
91
|
+
sqlite: Dialect.new(name: "SQLite", internals_key: 12)
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "delegate"
|
4
|
+
|
5
|
+
module Aikido::Zen
|
6
|
+
module Scanners
|
7
|
+
module SSRF
|
8
|
+
# Simple per-request cache of all DNS lookups performed for a given host.
|
9
|
+
# We can store this in the context after performing a lookup, and have the
|
10
|
+
# SSRF scanner make sure the hostname being inspected doesn't actually
|
11
|
+
# resolve to an internal/dangerous IP.
|
12
|
+
class DNSLookups < SimpleDelegator
|
13
|
+
def initialize
|
14
|
+
super(Hash.new { |h, k| h[k] = [] })
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(hostname, addresses)
|
18
|
+
self[hostname].concat(Array(addresses))
|
19
|
+
end
|
20
|
+
|
21
|
+
def ===(hostname)
|
22
|
+
key?(hostname)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "resolv"
|
4
|
+
require "ipaddr"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
module Scanners
|
8
|
+
module SSRF
|
9
|
+
# Little helper to check if a given hostname or address is to be
|
10
|
+
# considered "dangerous" when used for an outbound HTTP request.
|
11
|
+
#
|
12
|
+
# When given a hostname:
|
13
|
+
#
|
14
|
+
# * If any DNS lookups have been performed and stored in the current Zen
|
15
|
+
# context (under the "dns.lookups" metadata key), we will map it to the
|
16
|
+
# list of IPs that we've resolved it to.
|
17
|
+
#
|
18
|
+
# * If not, we'll still try to map it to any statically defined address in
|
19
|
+
# the system hosts file (e.g. /etc/hosts).
|
20
|
+
#
|
21
|
+
# Once we mapped the hostname to an IP address (or, if given an IP
|
22
|
+
# address), this will check that it's not a loopback address, a private IP
|
23
|
+
# address (as defined by RFCs 1918 and 4193), or in one of the
|
24
|
+
# "special-use" IP ranges defined in RFC 5735.
|
25
|
+
class PrivateIPChecker
|
26
|
+
def initialize(resolver = Resolv::Hosts.new)
|
27
|
+
@resolver = resolver
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param hostname_or_address [String]
|
31
|
+
# @return [Boolean]
|
32
|
+
def private?(hostname_or_address)
|
33
|
+
resolve(hostname_or_address).any? do |ip|
|
34
|
+
ip.loopback? || ip.private? || RFC5735.any? { |range| range === ip }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
RFC5735 = [
|
41
|
+
IPAddr.new("0.0.0.0/8"),
|
42
|
+
IPAddr.new("100.64.0.0/10"),
|
43
|
+
IPAddr.new("127.0.0.0/8"),
|
44
|
+
IPAddr.new("169.254.0.0/16"),
|
45
|
+
IPAddr.new("192.0.0.0/24"),
|
46
|
+
IPAddr.new("192.0.2.0/24"),
|
47
|
+
IPAddr.new("192.31.196.0/24"),
|
48
|
+
IPAddr.new("192.52.193.0/24"),
|
49
|
+
IPAddr.new("192.88.99.0/24"),
|
50
|
+
IPAddr.new("192.175.48.0/24"),
|
51
|
+
IPAddr.new("198.18.0.0/15"),
|
52
|
+
IPAddr.new("198.51.100.0/24"),
|
53
|
+
IPAddr.new("203.0.113.0/24"),
|
54
|
+
IPAddr.new("240.0.0.0/4"),
|
55
|
+
IPAddr.new("224.0.0.0/4"),
|
56
|
+
IPAddr.new("255.255.255.255/32"),
|
57
|
+
|
58
|
+
IPAddr.new("::/128"), # Unspecified address
|
59
|
+
IPAddr.new("fe80::/10"), # Link-local address (LLA)
|
60
|
+
IPAddr.new("::ffff:127.0.0.1/128") # IPv4-mapped address
|
61
|
+
]
|
62
|
+
|
63
|
+
def resolved_in_current_context
|
64
|
+
context = Aikido::Zen.current_context
|
65
|
+
context && context["dns.lookups"]
|
66
|
+
end
|
67
|
+
|
68
|
+
def resolve(hostname_or_address)
|
69
|
+
return [] if hostname_or_address.nil?
|
70
|
+
|
71
|
+
case hostname_or_address
|
72
|
+
when Resolv::AddressRegex
|
73
|
+
[IPAddr.new(hostname_or_address)]
|
74
|
+
when resolved_in_current_context
|
75
|
+
resolved_in_current_context[hostname_or_address]
|
76
|
+
.map { |address| IPAddr.new(address) }
|
77
|
+
else
|
78
|
+
@resolver.getaddresses(hostname_or_address.to_s)
|
79
|
+
.map { |address| IPAddr.new(address) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|