aikido-zen 1.0.2-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.aikido +6 -0
- data/.ruby-version +1 -0
- data/.simplecov +32 -0
- data/.standard.yml +3 -0
- data/LICENSE +674 -0
- data/README.md +148 -0
- data/Rakefile +67 -0
- data/benchmarks/README.md +22 -0
- data/benchmarks/rails7.1_benchmark.js +1 -0
- data/benchmarks/rails7.1_sql_injection.js +102 -0
- data/docs/banner.svg +202 -0
- data/docs/config.md +133 -0
- data/docs/proxy.md +10 -0
- data/docs/rails.md +112 -0
- data/docs/troubleshooting.md +62 -0
- data/lib/aikido/zen/actor.rb +146 -0
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +181 -0
- data/lib/aikido/zen/api_client.rb +145 -0
- data/lib/aikido/zen/attack.rb +217 -0
- data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
- data/lib/aikido/zen/attack_wave.rb +88 -0
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/cache.rb +91 -0
- data/lib/aikido/zen/capped_collections.rb +86 -0
- data/lib/aikido/zen/collector/event.rb +238 -0
- data/lib/aikido/zen/collector/hosts.rb +30 -0
- data/lib/aikido/zen/collector/routes.rb +71 -0
- data/lib/aikido/zen/collector/sink_stats.rb +95 -0
- data/lib/aikido/zen/collector/stats.rb +122 -0
- data/lib/aikido/zen/collector/users.rb +32 -0
- data/lib/aikido/zen/collector.rb +223 -0
- data/lib/aikido/zen/config.rb +312 -0
- data/lib/aikido/zen/context/rack_request.rb +27 -0
- data/lib/aikido/zen/context/rails_request.rb +47 -0
- data/lib/aikido/zen/context.rb +145 -0
- data/lib/aikido/zen/detached_agent/agent.rb +79 -0
- data/lib/aikido/zen/detached_agent/front_object.rb +41 -0
- data/lib/aikido/zen/detached_agent/server.rb +78 -0
- data/lib/aikido/zen/detached_agent.rb +2 -0
- data/lib/aikido/zen/errors.rb +107 -0
- data/lib/aikido/zen/event.rb +116 -0
- data/lib/aikido/zen/helpers.rb +24 -0
- data/lib/aikido/zen/internals.rb +123 -0
- data/lib/aikido/zen/libzen-v0.1.48-aarch64-linux.so +0 -0
- data/lib/aikido/zen/middleware/allowed_address_checker.rb +26 -0
- data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
- data/lib/aikido/zen/middleware/context_setter.rb +26 -0
- data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +50 -0
- data/lib/aikido/zen/middleware/request_tracker.rb +197 -0
- data/lib/aikido/zen/outbound_connection.rb +62 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +50 -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 +50 -0
- data/lib/aikido/zen/request/heuristic_router.rb +115 -0
- data/lib/aikido/zen/request/rails_router.rb +92 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
- data/lib/aikido/zen/request/schema/builder.rb +121 -0
- data/lib/aikido/zen/request/schema/definition.rb +107 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +87 -0
- data/lib/aikido/zen/request.rb +88 -0
- data/lib/aikido/zen/route.rb +96 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +78 -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 +66 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +68 -0
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +64 -0
- data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +65 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +94 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +266 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +55 -0
- data/lib/aikido/zen/scanners.rb +7 -0
- data/lib/aikido/zen/sink.rb +118 -0
- data/lib/aikido/zen/sinks/action_controller.rb +85 -0
- data/lib/aikido/zen/sinks/async_http.rb +80 -0
- data/lib/aikido/zen/sinks/curb.rb +113 -0
- data/lib/aikido/zen/sinks/em_http.rb +83 -0
- data/lib/aikido/zen/sinks/excon.rb +118 -0
- data/lib/aikido/zen/sinks/file.rb +153 -0
- data/lib/aikido/zen/sinks/http.rb +93 -0
- data/lib/aikido/zen/sinks/httpclient.rb +95 -0
- data/lib/aikido/zen/sinks/httpx.rb +78 -0
- data/lib/aikido/zen/sinks/kernel.rb +33 -0
- data/lib/aikido/zen/sinks/mysql2.rb +31 -0
- data/lib/aikido/zen/sinks/net_http.rb +101 -0
- data/lib/aikido/zen/sinks/patron.rb +103 -0
- data/lib/aikido/zen/sinks/pg.rb +72 -0
- data/lib/aikido/zen/sinks/resolv.rb +62 -0
- data/lib/aikido/zen/sinks/socket.rb +85 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
- data/lib/aikido/zen/sinks/trilogy.rb +31 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +36 -0
- data/lib/aikido/zen/sinks_dsl.rb +250 -0
- data/lib/aikido/zen/synchronizable.rb +24 -0
- data/lib/aikido/zen/system_info.rb +80 -0
- data/lib/aikido/zen/version.rb +10 -0
- data/lib/aikido/zen/worker.rb +87 -0
- data/lib/aikido/zen.rb +303 -0
- data/lib/aikido-zen.rb +3 -0
- 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 +94 -0
- data/tasklib/libzen.rake +133 -0
- data/tasklib/wrk.rb +88 -0
- metadata +214 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module Aikido::Zen
|
|
6
|
+
# Wrapper around Rack::Request-like objects to add some behavior.
|
|
7
|
+
class Request < SimpleDelegator
|
|
8
|
+
# @return [String] identifier of the framework handling this HTTP request.
|
|
9
|
+
attr_reader :framework
|
|
10
|
+
|
|
11
|
+
# @return [Aikido::Zen::Router]
|
|
12
|
+
attr_reader :router
|
|
13
|
+
|
|
14
|
+
# The current user, if set by the host app.
|
|
15
|
+
#
|
|
16
|
+
# @return [Aikido::Zen::Actor, nil]
|
|
17
|
+
# @see Aikido::Zen.track_user
|
|
18
|
+
attr_accessor :actor
|
|
19
|
+
|
|
20
|
+
def initialize(delegate, config = Aikido::Zen.config, framework:, router:)
|
|
21
|
+
super(delegate)
|
|
22
|
+
@config = config
|
|
23
|
+
@framework = framework
|
|
24
|
+
@router = router
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def __setobj__(delegate) # :nodoc:
|
|
28
|
+
super
|
|
29
|
+
@route = @normalized_header = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Aikido::Zen::Route] the framework route being requested.
|
|
33
|
+
def route
|
|
34
|
+
@route ||= @router.recognize(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Aikido::Zen::Request::Schema, nil]
|
|
38
|
+
def schema
|
|
39
|
+
@schema ||= Aikido::Zen::Request::Schema.build
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String] the IP address of the client making the request.
|
|
43
|
+
def client_ip
|
|
44
|
+
return @client_ip if @client_ip
|
|
45
|
+
|
|
46
|
+
if @config.client_ip_header
|
|
47
|
+
value = env[@config.client_ip_header]
|
|
48
|
+
if Resolv::AddressRegex.match?(value)
|
|
49
|
+
@client_ip = value
|
|
50
|
+
else
|
|
51
|
+
@config.logger.warn("Invalid IP address in custom client IP header `#{@config.client_ip_header}`: `#{value}`")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@client_ip ||= respond_to?(:remote_ip) ? remote_ip : ip
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Map the CGI-style env Hash into "pretty-looking" headers, preserving the
|
|
59
|
+
# values as-is. For example, HTTP_ACCEPT turns into "Accept", CONTENT_TYPE
|
|
60
|
+
# turns into "Content-Type", and HTTP_X_FORWARDED_FOR turns into
|
|
61
|
+
# "X-Forwarded-For".
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash<String, String>]
|
|
64
|
+
def normalized_headers
|
|
65
|
+
@normalized_headers ||= env.slice(*BLESSED_CGI_HEADERS)
|
|
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
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def as_json
|
|
74
|
+
{
|
|
75
|
+
method: request_method.upcase,
|
|
76
|
+
url: url,
|
|
77
|
+
ipAddress: client_ip,
|
|
78
|
+
userAgent: user_agent,
|
|
79
|
+
source: framework,
|
|
80
|
+
route: route&.path
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
BLESSED_CGI_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
require_relative "request/schema"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
# Routes keep information about the mapping defined in the current web
|
|
5
|
+
# framework to go from a given HTTP request to the code that handles said
|
|
6
|
+
# request.
|
|
7
|
+
class Route
|
|
8
|
+
def self.from_json(data)
|
|
9
|
+
new(
|
|
10
|
+
verb: data[:method],
|
|
11
|
+
path: data[:path]
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [String] the HTTP verb used to request this route.
|
|
16
|
+
attr_reader :verb
|
|
17
|
+
|
|
18
|
+
# @return [String] the URL pattern used to match request paths. For
|
|
19
|
+
# example "/users/:id".
|
|
20
|
+
attr_reader :path
|
|
21
|
+
|
|
22
|
+
def initialize(verb:, path:)
|
|
23
|
+
@verb = verb
|
|
24
|
+
@path = path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def as_json
|
|
28
|
+
{method: verb, path: path}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ==(other)
|
|
32
|
+
other.is_a?(Route) &&
|
|
33
|
+
other.verb == verb &&
|
|
34
|
+
other.path == path
|
|
35
|
+
end
|
|
36
|
+
alias_method :eql?, :==
|
|
37
|
+
|
|
38
|
+
def hash
|
|
39
|
+
[verb, path].hash
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Sort routes by wildcard matching order deterministically:
|
|
43
|
+
#
|
|
44
|
+
# 1. Exact path before wildcard path
|
|
45
|
+
# 2. Fewer wildcards in path relative to path length
|
|
46
|
+
# 3. Earliest wildcard position in path
|
|
47
|
+
# 4. Exact verb before wildcard verb
|
|
48
|
+
# 5. Lexicographic path (tie-break)
|
|
49
|
+
# 6. Lexicographic verb (tie-break)
|
|
50
|
+
#
|
|
51
|
+
# @return [Array] the sort key
|
|
52
|
+
def sort_key
|
|
53
|
+
@sort_key ||= begin
|
|
54
|
+
stars = []
|
|
55
|
+
i = -1
|
|
56
|
+
while (i = path.index("*", i + 1))
|
|
57
|
+
stars << i
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
[
|
|
61
|
+
stars.empty? ? 0 : 1,
|
|
62
|
+
stars.length - path.length,
|
|
63
|
+
stars,
|
|
64
|
+
(verb == "*") ? 1 : 0,
|
|
65
|
+
path,
|
|
66
|
+
verb
|
|
67
|
+
].freeze
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def match?(other)
|
|
72
|
+
other.is_a?(Route) &&
|
|
73
|
+
pattern(verb).match?(other.verb) &&
|
|
74
|
+
pattern(path).match?(other.path)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def inspect
|
|
78
|
+
"#<#{self.class.name} #{verb} #{path.inspect}>"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Construct a regular expression equivalent to the wildcard string,
|
|
82
|
+
# where '*' is the wildcard operator.
|
|
83
|
+
#
|
|
84
|
+
# The resulting pattern matches the entire input, allows an optional
|
|
85
|
+
# trailing slash, and is case-insensitive.
|
|
86
|
+
#
|
|
87
|
+
# All other special characters in the regular expression are escaped
|
|
88
|
+
# so that they are treated literally.
|
|
89
|
+
#
|
|
90
|
+
# @param string [String] wildcard string
|
|
91
|
+
# @return [Regexp] regular expression matching the wildcard string
|
|
92
|
+
private def pattern(string)
|
|
93
|
+
/^#{Regexp.escape(string).gsub("\\*", ".*")}\/?$/i
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
endpoint_pairs = Array(data).map do |value|
|
|
20
|
+
route = Route.new(verb: value["method"], path: value["route"])
|
|
21
|
+
settings = RuntimeSettings::ProtectionSettings.from_json(value)
|
|
22
|
+
[route, settings]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Sort endpoints by wildcard matching order
|
|
26
|
+
endpoint_pairs.sort_by! do |route, settings|
|
|
27
|
+
route.sort_key
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
new(endpoint_pairs.to_h)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param endpoints [Hash] the endpoints in wildcard matching order
|
|
34
|
+
# @return [Aikido::Zen::RuntimeSettings::Endpoints]
|
|
35
|
+
def initialize(endpoints = {})
|
|
36
|
+
@endpoints = endpoints
|
|
37
|
+
@endpoints.default = RuntimeSettings::ProtectionSettings.none
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param route [Aikido::Zen::Route]
|
|
41
|
+
# @return [Aikido::Zen::RuntimeSettings::ProtectionSettings]
|
|
42
|
+
def [](route)
|
|
43
|
+
return @endpoints[route] if @endpoints.key?(route)
|
|
44
|
+
|
|
45
|
+
# Wildcard endpoint matching
|
|
46
|
+
|
|
47
|
+
@endpoints.each do |pattern, settings|
|
|
48
|
+
return settings if pattern.match?(route)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@endpoints.default
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param route [Aikido::Zen::Route]
|
|
55
|
+
# @return [Array<Aikido::Zen::RuntimeSettings::ProtectionSettings>]
|
|
56
|
+
def match(route)
|
|
57
|
+
matches = []
|
|
58
|
+
|
|
59
|
+
@endpoints.each do |pattern, settings|
|
|
60
|
+
matches << settings if pattern.match?(route)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
matches << @endpoints.default if matches.empty?
|
|
64
|
+
|
|
65
|
+
matches
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @!visibility private
|
|
69
|
+
def ==(other)
|
|
70
|
+
other.is_a?(RuntimeSettings::Endpoints) && to_h == other.to_h
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @!visibility private
|
|
74
|
+
protected def to_h
|
|
75
|
+
@endpoints
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
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,66 @@
|
|
|
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
|
+
RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :allowed_ips, :received_any_stats, :blocking_mode) do
|
|
15
|
+
def initialize(*)
|
|
16
|
+
super
|
|
17
|
+
self.endpoints ||= RuntimeSettings::Endpoints.new
|
|
18
|
+
self.allowed_ips ||= RuntimeSettings::IPSet.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @!attribute [rw] updated_at
|
|
22
|
+
# @return [Time] when these settings were updated in the Aikido dashboard.
|
|
23
|
+
|
|
24
|
+
# @!attribute [rw] heartbeat_interval
|
|
25
|
+
# @return [Integer] duration in seconds between heartbeat requests to the
|
|
26
|
+
# Aikido server.
|
|
27
|
+
|
|
28
|
+
# @!attribute [rw] received_any_stats
|
|
29
|
+
# @return [Boolean] whether the Aikido server has received any data from
|
|
30
|
+
# this application.
|
|
31
|
+
|
|
32
|
+
# @!attribute [rw] endpoints
|
|
33
|
+
# @return [Aikido::Zen::RuntimeSettings::Endpoints]
|
|
34
|
+
|
|
35
|
+
# @!attribute [rw] blocked_user_ids
|
|
36
|
+
# @return [Array]
|
|
37
|
+
|
|
38
|
+
# @!attribute [rw] allowed_ips
|
|
39
|
+
# @return [Aikido::Zen::RuntimeSettings::IPSet]
|
|
40
|
+
|
|
41
|
+
# Parse and interpret the JSON response from the core API with updated
|
|
42
|
+
# settings, and apply the changes. This will also notify any subscriber
|
|
43
|
+
# to updates
|
|
44
|
+
#
|
|
45
|
+
# @param data [Hash] the decoded JSON payload from the /api/runtime/config
|
|
46
|
+
# API endpoint.
|
|
47
|
+
#
|
|
48
|
+
# @return [bool]
|
|
49
|
+
def update_from_json(data)
|
|
50
|
+
last_updated_at = updated_at
|
|
51
|
+
|
|
52
|
+
self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
|
|
53
|
+
self.heartbeat_interval = data["heartbeatIntervalInMS"].to_i / 1000
|
|
54
|
+
self.endpoints = RuntimeSettings::Endpoints.from_json(data["endpoints"])
|
|
55
|
+
self.blocked_user_ids = data["blockedUserIds"]
|
|
56
|
+
self.allowed_ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
|
|
57
|
+
self.received_any_stats = data["receivedAnyStats"]
|
|
58
|
+
self.blocking_mode = data["block"]
|
|
59
|
+
|
|
60
|
+
updated_at != last_updated_at
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
require_relative "runtime_settings/ip_set"
|
|
66
|
+
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,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
module Scanners
|
|
5
|
+
module PathTraversal
|
|
6
|
+
DANGEROUS_PATH_PARTS = ["../", "..\\"]
|
|
7
|
+
|
|
8
|
+
LINUX_PATH_STARTS = [
|
|
9
|
+
"/bin/",
|
|
10
|
+
"/boot/",
|
|
11
|
+
"/dev/",
|
|
12
|
+
"/etc/",
|
|
13
|
+
"/home/",
|
|
14
|
+
"/init/",
|
|
15
|
+
"/lib/",
|
|
16
|
+
"/media/",
|
|
17
|
+
"/mnt/",
|
|
18
|
+
"/opt/",
|
|
19
|
+
"/proc/",
|
|
20
|
+
"/root/",
|
|
21
|
+
"/run/",
|
|
22
|
+
"/sbin/",
|
|
23
|
+
"/srv/",
|
|
24
|
+
"/sys/",
|
|
25
|
+
"/tmp/",
|
|
26
|
+
"/usr/",
|
|
27
|
+
"/var/"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
WINDOWS_PATH_STARTS = ["c:/", "c:\\"]
|
|
31
|
+
|
|
32
|
+
DANGEROUS_PATH_STARTS = LINUX_PATH_STARTS + WINDOWS_PATH_STARTS
|
|
33
|
+
|
|
34
|
+
module Helpers
|
|
35
|
+
def self.include_unsafe_path_parts?(filepath)
|
|
36
|
+
DANGEROUS_PATH_PARTS.each do |dangerous_part|
|
|
37
|
+
return true if filepath.include?(dangerous_part)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.start_with_unsafe_path?(filepath, user_input)
|
|
44
|
+
# Check if path is relative (not absolute or drive letter path)
|
|
45
|
+
# Required because `expand_path` will build absolute paths from relative paths
|
|
46
|
+
return false if Pathname.new(filepath).relative? || Pathname.new(user_input).relative?
|
|
47
|
+
|
|
48
|
+
normalized_path = File.expand_path__internal_for_aikido_zen(filepath).downcase
|
|
49
|
+
normalized_user_input = File.expand_path__internal_for_aikido_zen(user_input).downcase
|
|
50
|
+
|
|
51
|
+
DANGEROUS_PATH_STARTS.each do |dangerous_start|
|
|
52
|
+
if normalized_path.start_with?(dangerous_start) && normalized_path.start_with?(normalized_user_input)
|
|
53
|
+
# If the user input is the same as the dangerous start, we don't want to flag it
|
|
54
|
+
# to prevent false positives.
|
|
55
|
+
# e.g., if user input is /etc/ and the path is /etc/passwd, we don't want to flag it,
|
|
56
|
+
# as long as the user input does not contain a subdirectory or filename
|
|
57
|
+
return false if user_input == dangerous_start || user_input == dangerous_start.chomp("/")
|
|
58
|
+
|
|
59
|
+
return true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "path_traversal/helpers"
|
|
4
|
+
|
|
5
|
+
module Aikido::Zen
|
|
6
|
+
module Scanners
|
|
7
|
+
class PathTraversalScanner
|
|
8
|
+
def self.skips_on_nil_context?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Checks if the user introduced input is trying to access other path using
|
|
13
|
+
# Path Traversal kind of attacks.
|
|
14
|
+
#
|
|
15
|
+
# @param filepath [String] the expanded path that is tried to be read
|
|
16
|
+
# @param context [Aikido::Zen::Context]
|
|
17
|
+
# @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
|
|
18
|
+
# @param operation [Symbol, String] name of the method being scanned.
|
|
19
|
+
#
|
|
20
|
+
# @return [Aikido::Zen::Attacks::PathTraversalAttack, nil] an Attack if any
|
|
21
|
+
# user input is detected to be attempting a Path Traversal Attack, or +nil+ if not.
|
|
22
|
+
def self.call(filepath:, sink:, context:, operation:)
|
|
23
|
+
context.payloads.each do |payload|
|
|
24
|
+
next unless new(filepath, payload.value.to_s).attack?
|
|
25
|
+
|
|
26
|
+
return Attacks::PathTraversalAttack.new(
|
|
27
|
+
sink: sink,
|
|
28
|
+
input: payload,
|
|
29
|
+
filepath: filepath,
|
|
30
|
+
context: context,
|
|
31
|
+
operation: "#{sink.operation}.#{operation}",
|
|
32
|
+
stack: Aikido::Zen.clean_stack_trace
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(filepath, input)
|
|
40
|
+
@filepath = filepath.downcase
|
|
41
|
+
@input = input.downcase
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def attack?
|
|
45
|
+
# Single character are ignored because they don't pose a big threat
|
|
46
|
+
return false if @input.length <= 1
|
|
47
|
+
|
|
48
|
+
# We ignore cases where the user input is longer than the file path.
|
|
49
|
+
# Because the user input can't be part of the file path.
|
|
50
|
+
return false if @input.length > @filepath.length
|
|
51
|
+
|
|
52
|
+
# We ignore cases where the user input is not part of the file path.
|
|
53
|
+
return false unless @filepath.include?(@input)
|
|
54
|
+
|
|
55
|
+
if PathTraversal::Helpers.include_unsafe_path_parts?(@filepath) && PathTraversal::Helpers.include_unsafe_path_parts?(@input)
|
|
56
|
+
return true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check for absolute path traversal
|
|
60
|
+
PathTraversal::Helpers.start_with_unsafe_path?(@filepath, @input)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|