aikido-zen 1.0.1.beta.4-arm64-darwin → 1.0.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +6 -0
  3. data/README.md +2 -0
  4. data/benchmarks/README.md +0 -1
  5. data/benchmarks/rails7.1_benchmark.js +1 -0
  6. data/benchmarks/rails7.1_sql_injection.js +52 -20
  7. data/docs/config.md +9 -1
  8. data/docs/proxy.md +10 -0
  9. data/docs/rails.md +55 -13
  10. data/docs/troubleshooting.md +62 -0
  11. data/lib/aikido/zen/actor.rb +34 -4
  12. data/lib/aikido/zen/agent/heartbeats_manager.rb +5 -5
  13. data/lib/aikido/zen/agent.rb +19 -17
  14. data/lib/aikido/zen/attack.rb +19 -9
  15. data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
  16. data/lib/aikido/zen/attack_wave.rb +88 -0
  17. data/lib/aikido/zen/cache.rb +91 -0
  18. data/lib/aikido/zen/capped_collections.rb +22 -4
  19. data/lib/aikido/zen/collector/event.rb +238 -0
  20. data/lib/aikido/zen/collector/hosts.rb +16 -1
  21. data/lib/aikido/zen/collector/routes.rb +13 -8
  22. data/lib/aikido/zen/collector/stats.rb +33 -22
  23. data/lib/aikido/zen/collector/users.rb +5 -3
  24. data/lib/aikido/zen/collector.rb +107 -28
  25. data/lib/aikido/zen/config.rb +54 -21
  26. data/lib/aikido/zen/context/rack_request.rb +3 -0
  27. data/lib/aikido/zen/context/rails_request.rb +3 -0
  28. data/lib/aikido/zen/context.rb +42 -9
  29. data/lib/aikido/zen/detached_agent/agent.rb +28 -27
  30. data/lib/aikido/zen/detached_agent/front_object.rb +10 -6
  31. data/lib/aikido/zen/detached_agent/server.rb +63 -26
  32. data/lib/aikido/zen/event.rb +47 -2
  33. data/lib/aikido/zen/helpers.rb +24 -0
  34. data/lib/aikido/zen/internals.rb +23 -3
  35. data/lib/aikido/zen/libzen-v0.1.48-arm64-darwin.dylib +0 -0
  36. data/lib/aikido/zen/middleware/{check_allowed_addresses.rb → allowed_address_checker.rb} +1 -1
  37. data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
  38. data/lib/aikido/zen/middleware/{set_context.rb → context_setter.rb} +1 -1
  39. data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
  40. data/lib/aikido/zen/middleware/rack_throttler.rb +3 -1
  41. data/lib/aikido/zen/middleware/request_tracker.rb +9 -4
  42. data/lib/aikido/zen/outbound_connection.rb +18 -1
  43. data/lib/aikido/zen/payload.rb +1 -1
  44. data/lib/aikido/zen/rails_engine.rb +5 -8
  45. data/lib/aikido/zen/request/rails_router.rb +17 -2
  46. data/lib/aikido/zen/request.rb +21 -36
  47. data/lib/aikido/zen/route.rb +57 -0
  48. data/lib/aikido/zen/runtime_settings/endpoints.rb +37 -8
  49. data/lib/aikido/zen/runtime_settings.rb +6 -5
  50. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +10 -7
  51. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +5 -4
  52. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +3 -2
  53. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +3 -2
  54. data/lib/aikido/zen/scanners/ssrf_scanner.rb +2 -1
  55. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +8 -2
  56. data/lib/aikido/zen/sink.rb +1 -1
  57. data/lib/aikido/zen/sinks/action_controller.rb +3 -1
  58. data/lib/aikido/zen/sinks/async_http.rb +40 -42
  59. data/lib/aikido/zen/sinks/curb.rb +56 -58
  60. data/lib/aikido/zen/sinks/em_http.rb +27 -29
  61. data/lib/aikido/zen/sinks/excon.rb +62 -65
  62. data/lib/aikido/zen/sinks/file.rb +108 -71
  63. data/lib/aikido/zen/sinks/http.rb +26 -28
  64. data/lib/aikido/zen/sinks/httpclient.rb +27 -29
  65. data/lib/aikido/zen/sinks/httpx.rb +27 -29
  66. data/lib/aikido/zen/sinks/kernel.rb +11 -12
  67. data/lib/aikido/zen/sinks/mysql2.rb +10 -12
  68. data/lib/aikido/zen/sinks/net_http.rb +25 -27
  69. data/lib/aikido/zen/sinks/patron.rb +56 -58
  70. data/lib/aikido/zen/sinks/pg.rb +23 -25
  71. data/lib/aikido/zen/sinks/resolv.rb +21 -21
  72. data/lib/aikido/zen/sinks/socket.rb +17 -12
  73. data/lib/aikido/zen/sinks/sqlite3.rb +18 -21
  74. data/lib/aikido/zen/sinks/trilogy.rb +10 -12
  75. data/lib/aikido/zen/sinks.rb +1 -4
  76. data/lib/aikido/zen/sinks_dsl.rb +39 -15
  77. data/lib/aikido/zen/system_info.rb +1 -5
  78. data/lib/aikido/zen/version.rb +2 -2
  79. data/lib/aikido/zen.rb +78 -16
  80. data/tasklib/bench.rake +1 -1
  81. data/tasklib/libzen.rake +1 -0
  82. metadata +15 -5
  83. data/lib/aikido/zen/libzen-v0.1.39-arm64-darwin.dylib +0 -0
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido
4
+ module Zen
5
+ module Middleware
6
+ # This middleware is responsible for detecting when a process has forked
7
+ # (e.g., in a Puma or Unicorn worker) and resetting the state of the
8
+ # Aikido Zen agent. It should be inserted early in the middleware stack.
9
+ class ForkDetector
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ # This is the single, reliable trigger point for the fork check.
16
+ Aikido::Zen.check_and_handle_fork
17
+
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -33,8 +33,10 @@ module Aikido::Zen
33
33
  private
34
34
 
35
35
  def should_throttle?(request)
36
+ # Bypass rate limiting for allowed IPs
37
+ return false if @settings.allowed_ips.include?(request.ip)
38
+
36
39
  return false unless @settings.endpoints[request.route].rate_limiting.enabled?
37
- return false if @settings.skip_protection_for_ips.include?(request.ip)
38
40
 
39
41
  result = @detached_agent.calculate_rate_limits(request)
40
42
 
@@ -5,8 +5,9 @@ module Aikido::Zen
5
5
  # Rack middleware used to track request
6
6
  # It implements the logic under that which is considered worthy of being tracked.
7
7
  class RequestTracker
8
- def initialize(app)
8
+ def initialize(app, settings: Aikido::Zen.runtime_settings)
9
9
  @app = app
10
+ @settings = settings
10
11
  end
11
12
 
12
13
  def call(env)
@@ -16,9 +17,10 @@ module Aikido::Zen
16
17
  if request.route && track?(
17
18
  status_code: response[0],
18
19
  route: request.route.path,
19
- http_method: request.request_method
20
+ http_method: request.request_method,
21
+ ip: request.ip
20
22
  )
21
- Aikido::Zen.track_request request
23
+ Aikido::Zen.track_request(request)
22
24
 
23
25
  if Aikido::Zen.config.collect_api_schema?
24
26
  Aikido::Zen.track_discovered_route(request)
@@ -126,7 +128,10 @@ module Aikido::Zen
126
128
  # @param status_code [Integer]
127
129
  # @param route [String]
128
130
  # @param http_method [String]
129
- def track?(status_code:, route:, http_method:)
131
+ def track?(status_code:, route:, http_method:, ip: nil)
132
+ # Bypass request and route tracking for allowed IPs
133
+ return false if @settings.allowed_ips.include?(ip)
134
+
130
135
  # In the UI we want to show only successful (2xx) or redirect (3xx) responses
131
136
  # anything else is discarded.
132
137
  return false unless status_code >= 200 && status_code <= 399
@@ -3,6 +3,13 @@
3
3
  module Aikido::Zen
4
4
  # Simple data object to identify connections performed to outbound servers.
5
5
  class OutboundConnection
6
+ def self.from_json(data)
7
+ new(
8
+ host: data[:hostname],
9
+ port: data[:port]
10
+ )
11
+ end
12
+
6
13
  # Convenience factory to create connection descriptions out of URI objects.
7
14
  #
8
15
  # @param uri [URI]
@@ -18,13 +25,23 @@ module Aikido::Zen
18
25
  # @return [Integer] the port number to which the connection was attempted.
19
26
  attr_reader :port
20
27
 
28
+ # @return [Integer] the number of times that this connection was seen by
29
+ # the hosts collector.
30
+ attr_reader :hits
31
+
21
32
  def initialize(host:, port:)
22
33
  @host = host
23
34
  @port = port
24
35
  end
25
36
 
37
+ def hit
38
+ # Lazy initialize @hits, so it stays nil until the connection is tracked.
39
+ @hits ||= 0
40
+ @hits += 1
41
+ end
42
+
26
43
  def as_json
27
- {hostname: host, port: port}
44
+ {hostname: host, port: port, hits: hits}.compact
28
45
  end
29
46
 
30
47
  def ==(other)
@@ -27,7 +27,7 @@ module Aikido::Zen
27
27
  {
28
28
  payload: value.to_s,
29
29
  source: SOURCE_SERIALIZATIONS[source],
30
- pathToPayload: path.to_s
30
+ path: path.to_s
31
31
  }
32
32
  end
33
33
 
@@ -10,8 +10,11 @@ module Aikido::Zen
10
10
  end
11
11
 
12
12
  initializer "aikido.add_middleware" do |app|
13
- app.middleware.use Aikido::Zen::Middleware::SetContext
14
- app.middleware.use Aikido::Zen::Middleware::CheckAllowedAddresses
13
+ app.middleware.insert_before 0, Aikido::Zen::Middleware::ForkDetector
14
+
15
+ app.middleware.use Aikido::Zen::Middleware::ContextSetter
16
+ app.middleware.use Aikido::Zen::Middleware::AllowedAddressChecker
17
+ app.middleware.use Aikido::Zen::Middleware::AttackWaveProtector
15
18
  # Request Tracker stats do not consider failed request or 40x, so the middleware
16
19
  # must be the last one wrapping the request.
17
20
  app.middleware.use Aikido::Zen::Middleware::RequestTracker
@@ -29,12 +32,6 @@ module Aikido::Zen
29
32
  end
30
33
 
31
34
  initializer "aikido.configuration" do |app|
32
- # Allow the logger to be configured before checking if disabled? so we can
33
- # let the user know that the agent is disabled.
34
- logger = ::Rails.logger
35
- logger = logger.tagged("aikido") if logger.respond_to?(:tagged)
36
- app.config.zen.logger = logger
37
-
38
35
  app.config.zen.request_builder = Aikido::Zen::Context::RAILS_REQUEST_BUILDER
39
36
 
40
37
  # Plug Rails' JSON encoder/decoder, but only if the user hasn't changed
@@ -62,16 +62,31 @@ module Aikido::Zen
62
62
  nil
63
63
  end
64
64
 
65
- private def build_route(route, request, prefix: request.script_name)
65
+ private
66
+
67
+ def build_route(route, request, prefix: request.script_name)
66
68
  route_wrapper = ActionDispatch::Routing::RouteWrapper.new(route)
67
69
 
68
70
  path = if prefix.present?
69
- File.join(prefix.to_s, route_wrapper.path).chomp("/")
71
+ prefix_route_path(prefix.to_s, route_wrapper.path)
70
72
  else
71
73
  route_wrapper.path
72
74
  end
73
75
 
74
76
  Aikido::Zen::Route.new(verb: request.request_method, path: path)
75
77
  end
78
+
79
+ def prefix_route_path(string1, string2)
80
+ # The strings appear to start with "/", allowing them to be concatenated
81
+ # directly after removing trailing "/". However, as it is not currently
82
+ # known whether this is guaranteed, we insert a separator when necessary.
83
+
84
+ separator = string2.start_with?("/") ? "" : "/"
85
+
86
+ string1 = string1.chomp("/")
87
+ string2 = string2.chomp("/")
88
+
89
+ "#{string1}#{separator}#{string2}"
90
+ end
76
91
  end
77
92
  end
@@ -17,17 +17,16 @@ module Aikido::Zen
17
17
  # @see Aikido::Zen.track_user
18
18
  attr_accessor :actor
19
19
 
20
- def initialize(delegate, framework:, router:)
20
+ def initialize(delegate, config = Aikido::Zen.config, framework:, router:)
21
21
  super(delegate)
22
+ @config = config
22
23
  @framework = framework
23
24
  @router = router
24
- @body_read = false
25
25
  end
26
26
 
27
27
  def __setobj__(delegate) # :nodoc:
28
28
  super
29
- @body_read = false
30
- @route = @normalized_header = @truncated_body = nil
29
+ @route = @normalized_header = nil
31
30
  end
32
31
 
33
32
  # @return [Aikido::Zen::Route] the framework route being requested.
@@ -40,6 +39,22 @@ module Aikido::Zen
40
39
  @schema ||= Aikido::Zen::Request::Schema.build
41
40
  end
42
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
+
43
58
  # Map the CGI-style env Hash into "pretty-looking" headers, preserving the
44
59
  # values as-is. For example, HTTP_ACCEPT turns into "Accept", CONTENT_TYPE
45
60
  # turns into "Content-Type", and HTTP_X_FORWARDED_FOR turns into
@@ -55,42 +70,12 @@ module Aikido::Zen
55
70
  }
56
71
  end
57
72
 
58
- # @api private
59
- #
60
- # Reads the first 16KiB of the request body, to include in attack reports
61
- # back to the Aikido server. This method should only be called if an attack
62
- # is detected during the current request.
63
- #
64
- # If the underlying IO object has been partially (or fully) read before,
65
- # this will attempt to restore the previous cursor position after reading it
66
- # if possible, or leave if rewund if not.
67
- #
68
- # @param max_size [Integer] number of bytes to read at most.
69
- #
70
- # @return [String]
71
- def truncated_body(max_size: 16384)
72
- return @truncated_body if @body_read
73
- return nil if body.nil?
74
-
75
- begin
76
- initial_pos = body.pos if body.respond_to?(:pos)
77
- body.rewind
78
- @truncated_body = body.read(max_size)
79
- ensure
80
- @body_read = true
81
- body.rewind
82
- body.seek(initial_pos) if initial_pos && body.respond_to?(:seek)
83
- end
84
- end
85
-
86
73
  def as_json
87
74
  {
88
- method: request_method.downcase,
75
+ method: request_method.upcase,
89
76
  url: url,
90
- ipAddress: ip,
77
+ ipAddress: client_ip,
91
78
  userAgent: user_agent,
92
- headers: normalized_headers.reject { |_, val| val.to_s.empty? },
93
- body: truncated_body,
94
79
  source: framework,
95
80
  route: route&.path
96
81
  }
@@ -5,6 +5,13 @@ module Aikido::Zen
5
5
  # framework to go from a given HTTP request to the code that handles said
6
6
  # request.
7
7
  class Route
8
+ def self.from_json(data)
9
+ new(
10
+ verb: data[:method],
11
+ path: data[:path]
12
+ )
13
+ end
14
+
8
15
  # @return [String] the HTTP verb used to request this route.
9
16
  attr_reader :verb
10
17
 
@@ -32,8 +39,58 @@ module Aikido::Zen
32
39
  [verb, path].hash
33
40
  end
34
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
+
35
77
  def inspect
36
78
  "#<#{self.class.name} #{verb} #{path.inspect}>"
37
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
38
95
  end
39
96
  end
@@ -16,24 +16,53 @@ module Aikido::Zen
16
16
  # @param data [Array<Hash>]
17
17
  # @return [Aikido::Zen::RuntimeSettings::Endpoints]
18
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)
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
22
  [route, settings]
23
- }.to_h
23
+ end
24
24
 
25
- new(data)
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)
26
31
  end
27
32
 
28
- def initialize(data = {})
29
- @endpoints = data
33
+ # @param endpoints [Hash] the endpoints in wildcard matching order
34
+ # @return [Aikido::Zen::RuntimeSettings::Endpoints]
35
+ def initialize(endpoints = {})
36
+ @endpoints = endpoints
30
37
  @endpoints.default = RuntimeSettings::ProtectionSettings.none
31
38
  end
32
39
 
33
40
  # @param route [Aikido::Zen::Route]
34
41
  # @return [Aikido::Zen::RuntimeSettings::ProtectionSettings]
35
42
  def [](route)
36
- @endpoints[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
37
66
  end
38
67
 
39
68
  # @!visibility private
@@ -11,11 +11,11 @@ module Aikido::Zen
11
11
  #
12
12
  # You can subscribe to changes with +#add_observer(object, func_name)+, which
13
13
  # will call the function passing the settings as an argument.
14
- RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :skip_protection_for_ips, :received_any_stats) do
14
+ RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :allowed_ips, :received_any_stats, :blocking_mode) do
15
15
  def initialize(*)
16
16
  super
17
17
  self.endpoints ||= RuntimeSettings::Endpoints.new
18
- self.skip_protection_for_ips ||= RuntimeSettings::IPSet.new
18
+ self.allowed_ips ||= RuntimeSettings::IPSet.new
19
19
  end
20
20
 
21
21
  # @!attribute [rw] updated_at
@@ -35,7 +35,7 @@ module Aikido::Zen
35
35
  # @!attribute [rw] blocked_user_ids
36
36
  # @return [Array]
37
37
 
38
- # @!attribute [rw] skip_protection_for_ips
38
+ # @!attribute [rw] allowed_ips
39
39
  # @return [Aikido::Zen::RuntimeSettings::IPSet]
40
40
 
41
41
  # Parse and interpret the JSON response from the core API with updated
@@ -50,11 +50,12 @@ module Aikido::Zen
50
50
  last_updated_at = updated_at
51
51
 
52
52
  self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
53
- self.heartbeat_interval = (data["heartbeatIntervalInMS"].to_i / 1000)
53
+ self.heartbeat_interval = data["heartbeatIntervalInMS"].to_i / 1000
54
54
  self.endpoints = RuntimeSettings::Endpoints.from_json(data["endpoints"])
55
55
  self.blocked_user_ids = data["blockedUserIds"]
56
- self.skip_protection_for_ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
56
+ self.allowed_ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
57
57
  self.received_any_stats = data["receivedAnyStats"]
58
+ self.blocking_mode = data["block"]
58
59
 
59
60
  updated_at != last_updated_at
60
61
  end
@@ -4,7 +4,8 @@ module Aikido::Zen
4
4
  module Scanners
5
5
  module PathTraversal
6
6
  DANGEROUS_PATH_PARTS = ["../", "..\\"]
7
- LINUX_ROOT_FOLDERS = [
7
+
8
+ LINUX_PATH_STARTS = [
8
9
  "/bin/",
9
10
  "/boot/",
10
11
  "/dev/",
@@ -26,10 +27,12 @@ module Aikido::Zen
26
27
  "/var/"
27
28
  ]
28
29
 
29
- DANGEROUS_PATH_STARTS = LINUX_ROOT_FOLDERS + ["c:/", "c:\\"]
30
+ WINDOWS_PATH_STARTS = ["c:/", "c:\\"]
31
+
32
+ DANGEROUS_PATH_STARTS = LINUX_PATH_STARTS + WINDOWS_PATH_STARTS
30
33
 
31
34
  module Helpers
32
- def self.contains_unsafe_path_parts(filepath)
35
+ def self.include_unsafe_path_parts?(filepath)
33
36
  DANGEROUS_PATH_PARTS.each do |dangerous_part|
34
37
  return true if filepath.include?(dangerous_part)
35
38
  end
@@ -37,7 +40,7 @@ module Aikido::Zen
37
40
  false
38
41
  end
39
42
 
40
- def self.starts_with_unsafe_path(filepath, user_input)
43
+ def self.start_with_unsafe_path?(filepath, user_input)
41
44
  # Check if path is relative (not absolute or drive letter path)
42
45
  # Required because `expand_path` will build absolute paths from relative paths
43
46
  return false if Pathname.new(filepath).relative? || Pathname.new(user_input).relative?
@@ -51,12 +54,12 @@ module Aikido::Zen
51
54
  # to prevent false positives.
52
55
  # e.g., if user input is /etc/ and the path is /etc/passwd, we don't want to flag it,
53
56
  # as long as the user input does not contain a subdirectory or filename
54
- if user_input == dangerous_start || user_input == dangerous_start.chomp("/")
55
- return false
56
- end
57
+ return false if user_input == dangerous_start || user_input == dangerous_start.chomp("/")
58
+
57
59
  return true
58
60
  end
59
61
  end
62
+
60
63
  false
61
64
  end
62
65
  end
@@ -21,14 +21,15 @@ module Aikido::Zen
21
21
  # user input is detected to be attempting a Path Traversal Attack, or +nil+ if not.
22
22
  def self.call(filepath:, sink:, context:, operation:)
23
23
  context.payloads.each do |payload|
24
- next unless new(filepath, payload.value).attack?
24
+ next unless new(filepath, payload.value.to_s).attack?
25
25
 
26
26
  return Attacks::PathTraversalAttack.new(
27
27
  sink: sink,
28
28
  input: payload,
29
29
  filepath: filepath,
30
30
  context: context,
31
- operation: "#{sink.operation}.#{operation}"
31
+ operation: "#{sink.operation}.#{operation}",
32
+ stack: Aikido::Zen.clean_stack_trace
32
33
  )
33
34
  end
34
35
 
@@ -51,12 +52,12 @@ module Aikido::Zen
51
52
  # We ignore cases where the user input is not part of the file path.
52
53
  return false unless @filepath.include?(@input)
53
54
 
54
- if PathTraversal::Helpers.contains_unsafe_path_parts(@filepath) && PathTraversal::Helpers.contains_unsafe_path_parts(@input)
55
+ if PathTraversal::Helpers.include_unsafe_path_parts?(@filepath) && PathTraversal::Helpers.include_unsafe_path_parts?(@input)
55
56
  return true
56
57
  end
57
58
 
58
59
  # Check for absolute path traversal
59
- PathTraversal::Helpers.starts_with_unsafe_path(@filepath, @input)
60
+ PathTraversal::Helpers.start_with_unsafe_path?(@filepath, @input)
60
61
  end
61
62
  end
62
63
  end
@@ -16,14 +16,15 @@ module Aikido::Zen
16
16
  #
17
17
  def self.call(command:, sink:, context:, operation:)
18
18
  context.payloads.each do |payload|
19
- next unless new(command, payload.value).attack?
19
+ next unless new(command, payload.value.to_s).attack?
20
20
 
21
21
  return Attacks::ShellInjectionAttack.new(
22
22
  sink: sink,
23
23
  input: payload,
24
24
  command: command,
25
25
  context: context,
26
- operation: "#{sink.operation}.#{operation}"
26
+ operation: "#{sink.operation}.#{operation}",
27
+ stack: Aikido::Zen.clean_stack_trace
27
28
  )
28
29
  end
29
30
 
@@ -32,7 +32,7 @@ module Aikido::Zen
32
32
  end
33
33
 
34
34
  context.payloads.each do |payload|
35
- next unless new(query, payload.value, dialect).attack?
35
+ next unless new(query, payload.value.to_s, dialect).attack?
36
36
 
37
37
  return Attacks::SQLInjectionAttack.new(
38
38
  sink: sink,
@@ -40,7 +40,8 @@ module Aikido::Zen
40
40
  input: payload,
41
41
  dialect: dialect,
42
42
  context: context,
43
- operation: "#{sink.operation}.#{operation}"
43
+ operation: "#{sink.operation}.#{operation}",
44
+ stack: Aikido::Zen.clean_stack_trace
44
45
  )
45
46
  end
46
47
 
@@ -51,7 +51,8 @@ module Aikido::Zen
51
51
  request: request,
52
52
  input: payload,
53
53
  context: context,
54
- operation: "#{sink.operation}.#{operation}"
54
+ operation: "#{sink.operation}.#{operation}",
55
+ stack: Aikido::Zen.clean_stack_trace
55
56
  )
56
57
 
57
58
  return attack
@@ -20,7 +20,8 @@ module Aikido::Zen
20
20
  address: offending_address,
21
21
  sink: sink,
22
22
  context: context,
23
- operation: "#{sink.operation}.#{operation}"
23
+ operation: "#{sink.operation}.#{operation}",
24
+ stack: Aikido::Zen.clean_stack_trace
24
25
  )
25
26
  end
26
27
 
@@ -33,7 +34,9 @@ module Aikido::Zen
33
34
  # @return [String, nil] either the offending address, or +nil+ if no
34
35
  # address is deemed dangerous.
35
36
  def attack?
36
- return false if @config.imds_allowed_hosts.include?(@hostname)
37
+ return unless @config.stored_ssrf? # Feature flag
38
+
39
+ return if @config.imds_allowed_hosts.include?(@hostname)
37
40
 
38
41
  @addresses.find do |candidate|
39
42
  DANGEROUS_ADDRESSES.any? { |address| address === candidate }
@@ -42,6 +45,9 @@ module Aikido::Zen
42
45
 
43
46
  DANGEROUS_ADDRESSES = [
44
47
  IPAddr.new("169.254.169.254"),
48
+ IPAddr.new("100.100.100.200"),
49
+ IPAddr.new("::ffff:169.254.169.254"),
50
+ IPAddr.new("::ffff:100.100.100.200"),
45
51
  IPAddr.new("fd00:ec2::254")
46
52
  ]
47
53
  end
@@ -4,7 +4,7 @@ require_relative "scan"
4
4
 
5
5
  module Aikido::Zen
6
6
  module Sinks
7
- # @api internal
7
+ # @api private
8
8
  # @return [Hash<String, Sink>]
9
9
  def self.registry
10
10
  @registry ||= {}
@@ -43,8 +43,10 @@ module Aikido::Zen
43
43
  end
44
44
 
45
45
  private def should_throttle?(request)
46
+ # Bypass rate limiting for allowed IPs
47
+ return false if @settings.allowed_ips.include?(request.ip)
48
+
46
49
  return false unless @settings.endpoints[request.route].rate_limiting.enabled?
47
- return false if @settings.skip_protection_for_ips.include?(request.ip)
48
50
 
49
51
  result = @detached_agent.calculate_rate_limits(request)
50
52
  return false unless result