aikido-zen 0.1.1-x86_64-linux → 0.2.0-x86_64-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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +1 -0
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +11 -2
  5. data/benchmarks/rails7.1_sql_injection.js +30 -34
  6. data/docs/banner.svg +128 -129
  7. data/docs/config.md +8 -6
  8. data/docs/rails.md +2 -2
  9. data/lib/aikido/zen/agent.rb +3 -1
  10. data/lib/aikido/zen/api_client.rb +3 -3
  11. data/lib/aikido/zen/attack.rb +105 -36
  12. data/lib/aikido/zen/collector/routes.rb +2 -0
  13. data/lib/aikido/zen/collector.rb +19 -3
  14. data/lib/aikido/zen/config.rb +44 -20
  15. data/lib/aikido/zen/errors.rb +10 -1
  16. data/lib/aikido/zen/event.rb +4 -2
  17. data/lib/aikido/zen/libzen-v0.1.37.x86_64.so +0 -0
  18. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +2 -14
  19. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  20. data/lib/aikido/zen/middleware/{throttler.rb → rack_throttler.rb} +3 -11
  21. data/lib/aikido/zen/middleware/request_tracker.rb +190 -0
  22. data/lib/aikido/zen/middleware/set_context.rb +1 -4
  23. data/lib/aikido/zen/payload.rb +2 -0
  24. data/lib/aikido/zen/rails_engine.rb +8 -0
  25. data/lib/aikido/zen/rate_limiter.rb +1 -1
  26. data/lib/aikido/zen/request/schema/builder.rb +0 -2
  27. data/lib/aikido/zen/request/schema/definition.rb +0 -5
  28. data/lib/aikido/zen/request/schema.rb +0 -3
  29. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  30. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +61 -0
  31. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  32. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +62 -0
  33. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +0 -4
  34. data/lib/aikido/zen/scanners/ssrf_scanner.rb +9 -6
  35. data/lib/aikido/zen/scanners.rb +2 -0
  36. data/lib/aikido/zen/sinks/action_controller.rb +26 -12
  37. data/lib/aikido/zen/sinks/file.rb +120 -0
  38. data/lib/aikido/zen/sinks/kernel.rb +73 -0
  39. data/lib/aikido/zen/sinks.rb +8 -0
  40. data/lib/aikido/zen/system_info.rb +1 -1
  41. data/lib/aikido/zen/version.rb +2 -2
  42. data/lib/aikido/zen.rb +14 -1
  43. data/tasklib/bench.rake +3 -2
  44. metadata +16 -8
  45. data/lib/aikido/zen/libzen-v0.1.31.x86_64.so +0 -0
@@ -3,12 +3,12 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
5
  module ActionController
6
- # Implements the "middleware" for rate limiting in Rails apps, where we
7
- # need to check at the end of the `before_action` chain, rather than in
8
- # an actual Rack middleware, to allow for calls to Zen.track_user being
9
- # made from before_actions in the host app, thus allowing rate-limiting
10
- # by user ID rather than solely by IP.
11
- class Throttler
6
+ # Implements the "middleware" for blocking requests (i.e.: rate-limiting or blocking
7
+ # user/bots) in Rails apps, where we need to check at the end of the `before_action`
8
+ # chain, rather than in an actual Rack middleware, to allow for calls to
9
+ # `Zen.track_user` being made from before_actions in the host app, thus allowing
10
+ # block/rate-limit by user ID rather than solely by IP.
11
+ class BlockRequestChecker
12
12
  def initialize(
13
13
  config: Aikido::Zen.config,
14
14
  settings: Aikido::Zen.runtime_settings,
@@ -19,10 +19,18 @@ module Aikido::Zen
19
19
  @rate_limiter = rate_limiter
20
20
  end
21
21
 
22
- def throttle(controller)
22
+ def block?(controller)
23
23
  context = controller.request.env[Aikido::Zen::ENV_KEY]
24
24
  request = context.request
25
25
 
26
+ if should_block_user?(request)
27
+ status, headers, body = @config.blocked_responder.call(request, :user)
28
+ controller.headers.update(headers)
29
+ controller.render plain: Array(body).join, status: status
30
+
31
+ return true
32
+ end
33
+
26
34
  if should_throttle?(request)
27
35
  status, headers, body = @config.rate_limited_responder.call(request)
28
36
  controller.headers.update(headers)
@@ -39,10 +47,17 @@ module Aikido::Zen
39
47
 
40
48
  @rate_limiter.throttle?(request)
41
49
  end
50
+
51
+ # @param request [Aikido::Zen::Request]
52
+ private def should_block_user?(request)
53
+ return false if request.actor.nil?
54
+
55
+ @settings.blocked_user_ids&.include?(request.actor.id)
56
+ end
42
57
  end
43
58
 
44
- def self.throttler
45
- @throttler ||= Aikido::Zen::Sinks::ActionController::Throttler.new
59
+ def self.block_request_checker
60
+ @block_request_checker ||= Aikido::Zen::Sinks::ActionController::BlockRequestChecker.new
46
61
  end
47
62
 
48
63
  module Extensions
@@ -50,10 +65,9 @@ module Aikido::Zen
50
65
  return super unless kind == :process_action
51
66
 
52
67
  super do
53
- rate_limiter = Aikido::Zen::Sinks::ActionController.throttler
54
- throttled = rate_limiter.throttle(self)
68
+ checker = Aikido::Zen::Sinks::ActionController.block_request_checker
55
69
 
56
- yield if block_given? && !throttled
70
+ yield if block_given? && !checker.block?(self)
57
71
  end
58
72
  end
59
73
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Sinks
5
+ module File
6
+ SINK = Sinks.add("File", scanners: [
7
+ Aikido::Zen::Scanners::PathTraversalScanner
8
+ ])
9
+
10
+ module Extensions
11
+ def self.scan_path(filepath, operation)
12
+ SINK.scan(
13
+ filepath: filepath,
14
+ operation: operation
15
+ )
16
+ end
17
+
18
+ # Module to extend only the initializer method of `File` (`File.new`)
19
+ module Initiliazer
20
+ def initialize(filename, *, **)
21
+ Extensions.scan_path(filename, "new")
22
+ super
23
+ end
24
+ end
25
+
26
+ def open(filename, *, **)
27
+ Extensions.scan_path(filename, "open")
28
+ super
29
+ end
30
+
31
+ def read(filename, *)
32
+ Extensions.scan_path(filename, "read")
33
+ super
34
+ end
35
+
36
+ def write(filename, *, **)
37
+ Extensions.scan_path(filename, "write")
38
+ super
39
+ end
40
+
41
+ def join(*)
42
+ joined = super
43
+ Extensions.scan_path(joined, "join")
44
+ joined
45
+ end
46
+
47
+ def chmod(mode, *paths)
48
+ paths.each { |path| Extensions.scan_path(path, "chmod") }
49
+ super
50
+ end
51
+
52
+ def chown(user, group, *paths)
53
+ paths.each { |path| Extensions.scan_path(path, "chown") }
54
+ super
55
+ end
56
+
57
+ def rename(from, to)
58
+ Extensions.scan_path(from, "rename")
59
+ Extensions.scan_path(to, "rename")
60
+ super
61
+ end
62
+
63
+ def symlink(from, to)
64
+ Extensions.scan_path(from, "symlink")
65
+ Extensions.scan_path(to, "symlink")
66
+ super
67
+ end
68
+
69
+ def truncate(file_name, *)
70
+ Extensions.scan_path(file_name, "truncate")
71
+ super
72
+ end
73
+
74
+ def unlink(*args)
75
+ args.each do |arg|
76
+ Extensions.scan_path(arg, "unlink")
77
+ end
78
+ super
79
+ end
80
+
81
+ def delete(*args)
82
+ args.each do |arg|
83
+ Extensions.scan_path(arg, "delete")
84
+ end
85
+ super
86
+ end
87
+
88
+ def utime(atime, mtime, *args)
89
+ args.each do |arg|
90
+ Extensions.scan_path(arg, "utime")
91
+ end
92
+ super
93
+ end
94
+
95
+ def expand_path(filename, *)
96
+ Extensions.scan_path(filename, "expand_path")
97
+ super
98
+ end
99
+
100
+ def realpath(filename, *)
101
+ Extensions.scan_path(filename, "realpath")
102
+ super
103
+ end
104
+
105
+ def realdirpath(filename, *)
106
+ Extensions.scan_path(filename, "realdirpath")
107
+ super
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ # Internally, Path Traversal's scanner logic uses `expand_path`, in order to avoid recursion issues we keep
115
+ # a copy of the original method, only to be used internally.
116
+ # It's important to keep this line before prepend the Extensions module, otherwise the alias will call
117
+ # the extended method.
118
+ ::File.singleton_class.alias_method :expand_path__internal_for_aikido_zen, :expand_path
119
+ ::File.singleton_class.prepend(Aikido::Zen::Sinks::File::Extensions)
120
+ ::File.prepend Aikido::Zen::Sinks::File::Extensions::Initiliazer
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Sinks
5
+ module Kernel
6
+ SINK = Sinks.add("Kernel", scanners: [
7
+ Aikido::Zen::Scanners::ShellInjectionScanner
8
+ ])
9
+
10
+ module Extensions
11
+ # Checks if the user introduced input is trying to execute other commands
12
+ # using Shell Injection kind of attacks.
13
+ #
14
+ # @param command [String] the _full command_ that will be executed.
15
+ # @param context [Aikido::Zen::Context]
16
+ # @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
17
+ # @param operation [Symbol, String] name of the method being scanned.
18
+ #
19
+ # @return [Aikido::Zen::Attacks::ShellInjectionAttack, nil] an Attack if any
20
+ # user input is detected as part of a Shell Injection Attack, or +nil+ if it's safe.
21
+ def self.scan_command(command, operation)
22
+ SINK.scan(
23
+ command: command,
24
+ operation: operation
25
+ )
26
+ end
27
+
28
+ # `system, spawn` functions can be invoked in several ways. For more details,
29
+ # see [the documentation](https://ruby-doc.org/3.4.1/Kernel.html#method-i-spawn)
30
+ #
31
+ # In our context, we care primarily about two common scenarios:
32
+ # - one argument (String)
33
+ # e.g.: system("ls"), system("echo something")
34
+ # - two arguments (Hash, String)
35
+ # e.g.: system({"foo" => "bar"}, "ls"), system({"foo" => "bar"}, "echo something")
36
+ #
37
+ # In all other cases, we do not protect against shell argument injections. Specifically:
38
+ #
39
+ # If a user input contains something like $(whoami) and is passed as part of the command
40
+ # arguments (e.g., user_input = "$(whoami)"):
41
+ #
42
+ # system("echo", user_input) This is safe because Ruby automatically escapes arguments
43
+ # passed to system/spawn in this form.
44
+ #
45
+ # system("echo #{user_input}") This is not safe because Ruby interpolates the user_input
46
+ # into the command string, resulting in a potentially harmful
47
+ # command like `echo $(whoami)`.
48
+ def send_arg_to_scan(args, operation)
49
+ if args.size == 1 && args[0].is_a?(String)
50
+ Extensions.scan_command(args[0], operation)
51
+ end
52
+
53
+ if args.size == 2 && args[0].is_a?(Hash)
54
+ Extensions.scan_command(args[1], operation)
55
+ end
56
+ end
57
+
58
+ def system(*args, **)
59
+ send_arg_to_scan(args, "system")
60
+ super
61
+ end
62
+
63
+ def spawn(*args, **)
64
+ send_arg_to_scan(args, "spawn")
65
+ super
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ ::Kernel.singleton_class.prepend Aikido::Zen::Sinks::Kernel::Extensions
73
+ ::Kernel.prepend Aikido::Zen::Sinks::Kernel::Extensions
@@ -5,6 +5,14 @@ require_relative "sink"
5
5
  require_relative "sinks/socket"
6
6
 
7
7
  require_relative "sinks/action_controller" if defined?(::ActionController)
8
+ require_relative "sinks/file" if defined?(::File)
9
+
10
+ # Sadly, in ruby versions lower than 3.0, it's not possible to patch the
11
+ # Kernel module because how the `prepend` method is applied
12
+ # (https://stackoverflow.com/questions/78110397/prepend-kernel-module-function-globally#comment137713906_78112924)
13
+ if RUBY_VERSION >= "3.0"
14
+ require_relative "sinks/kernel" if defined?(::Kernel)
15
+ end
8
16
  require_relative "sinks/resolv" if defined?(::Resolv)
9
17
  require_relative "sinks/net_http" if defined?(::Net::HTTP)
10
18
  require_relative "sinks/http" if defined?(::HTTP)
@@ -60,7 +60,7 @@ module Aikido::Zen
60
60
  end
61
61
 
62
62
  def os_version
63
- Gem::Platform.local.version
63
+ Gem::Platform.local.version || "unknown"
64
64
  end
65
65
 
66
66
  def as_json
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Aikido
4
4
  module Zen
5
- VERSION = "0.1.1"
5
+ VERSION = "0.2.0"
6
6
 
7
7
  # The version of libzen_internals that we build against.
8
- LIBZEN_VERSION = "0.1.31"
8
+ LIBZEN_VERSION = "0.1.37"
9
9
  end
10
10
  end
data/lib/aikido/zen.rb CHANGED
@@ -10,13 +10,15 @@ require_relative "zen/worker"
10
10
  require_relative "zen/agent"
11
11
  require_relative "zen/api_client"
12
12
  require_relative "zen/context"
13
+ require_relative "zen/middleware/check_allowed_addresses"
14
+ require_relative "zen/middleware/middleware"
15
+ require_relative "zen/middleware/request_tracker"
13
16
  require_relative "zen/middleware/set_context"
14
17
  require_relative "zen/outbound_connection"
15
18
  require_relative "zen/outbound_connection_monitor"
16
19
  require_relative "zen/runtime_settings"
17
20
  require_relative "zen/rate_limiter"
18
21
  require_relative "zen/scanners"
19
- require_relative "zen/middleware/check_allowed_addresses"
20
22
  require_relative "zen/rails_engine" if defined?(::Rails)
21
23
 
22
24
  module Aikido
@@ -70,6 +72,11 @@ module Aikido
70
72
  collector.track_request(request)
71
73
  end
72
74
 
75
+ def self.track_discovered_route(request)
76
+ autostart
77
+ collector.track_route(request)
78
+ end
79
+
73
80
  # Tracks a network connection made to an external service.
74
81
  #
75
82
  # @param connection [Aikido::Zen::OutboundConnection]
@@ -113,6 +120,12 @@ module Aikido
113
120
  end
114
121
  end
115
122
 
123
+ # Marks that the Zen middleware was installed properly
124
+ # @return void
125
+ def self.middleware_installed!
126
+ collector.middleware_installed!
127
+ end
128
+
116
129
  # Load all sinks matching libraries loaded into memory. This method should
117
130
  # be called after all other dependencies have been loaded into memory (i.e.
118
131
  # at the end of the initialization process).
data/tasklib/bench.rake CHANGED
@@ -12,11 +12,12 @@ end
12
12
 
13
13
  def boot_server(dir, port:, env: {})
14
14
  env["PORT"] = port.to_s
15
+ env["SECRET_KEY_BASE"] = rand(36**64).to_s(36)
15
16
 
16
17
  Dir.chdir(dir) do
17
18
  SERVER_PIDS[port] = Process.spawn(
18
19
  env,
19
- "rails", "server", "--pid", "#{Dir.pwd}/tmp/pids/server.#{port}.pid",
20
+ "rails", "server", "--pid", "#{Dir.pwd}/tmp/pids/server.#{port}.pid", "-e", "production",
20
21
  out: "/dev/null"
21
22
  )
22
23
  rescue
@@ -61,7 +62,7 @@ Pathname.glob("sample_apps/*").select(&:directory?).each do |dir|
61
62
  end
62
63
 
63
64
  task :boot_unprotected_app do
64
- boot_server(dir, port: 3002, env: {"AIKIDO_DISABLE" => "true"})
65
+ boot_server(dir, port: 3002, env: {"AIKIDO_DISABLED" => "true"})
65
66
  end
66
67
  end
67
68
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aikido-zen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Nicolas Sanguinetti
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-19 00:00:00.000000000 Z
11
+ date: 2025-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -55,7 +55,7 @@ dependencies:
55
55
  - !ruby/object:Gem::Version
56
56
  version: '0'
57
57
  force_ruby_platform: false
58
- description:
58
+ description:
59
59
  email:
60
60
  - foca@foca.io
61
61
  executables: []
@@ -96,10 +96,12 @@ files:
96
96
  - lib/aikido/zen/errors.rb
97
97
  - lib/aikido/zen/event.rb
98
98
  - lib/aikido/zen/internals.rb
99
- - lib/aikido/zen/libzen-v0.1.31.x86_64.so
99
+ - lib/aikido/zen/libzen-v0.1.37.x86_64.so
100
100
  - lib/aikido/zen/middleware/check_allowed_addresses.rb
101
+ - lib/aikido/zen/middleware/middleware.rb
102
+ - lib/aikido/zen/middleware/rack_throttler.rb
103
+ - lib/aikido/zen/middleware/request_tracker.rb
101
104
  - lib/aikido/zen/middleware/set_context.rb
102
- - lib/aikido/zen/middleware/throttler.rb
103
105
  - lib/aikido/zen/outbound_connection.rb
104
106
  - lib/aikido/zen/outbound_connection_monitor.rb
105
107
  - lib/aikido/zen/package.rb
@@ -126,6 +128,10 @@ files:
126
128
  - lib/aikido/zen/runtime_settings/rate_limit_settings.rb
127
129
  - lib/aikido/zen/scan.rb
128
130
  - lib/aikido/zen/scanners.rb
131
+ - lib/aikido/zen/scanners/path_traversal/helpers.rb
132
+ - lib/aikido/zen/scanners/path_traversal_scanner.rb
133
+ - lib/aikido/zen/scanners/shell_injection/helpers.rb
134
+ - lib/aikido/zen/scanners/shell_injection_scanner.rb
129
135
  - lib/aikido/zen/scanners/sql_injection_scanner.rb
130
136
  - lib/aikido/zen/scanners/ssrf/dns_lookups.rb
131
137
  - lib/aikido/zen/scanners/ssrf/private_ip_checker.rb
@@ -138,9 +144,11 @@ files:
138
144
  - lib/aikido/zen/sinks/curb.rb
139
145
  - lib/aikido/zen/sinks/em_http.rb
140
146
  - lib/aikido/zen/sinks/excon.rb
147
+ - lib/aikido/zen/sinks/file.rb
141
148
  - lib/aikido/zen/sinks/http.rb
142
149
  - lib/aikido/zen/sinks/httpclient.rb
143
150
  - lib/aikido/zen/sinks/httpx.rb
151
+ - lib/aikido/zen/sinks/kernel.rb
144
152
  - lib/aikido/zen/sinks/mysql2.rb
145
153
  - lib/aikido/zen/sinks/net_http.rb
146
154
  - lib/aikido/zen/sinks/patron.rb
@@ -163,7 +171,7 @@ metadata:
163
171
  homepage_uri: https://aikido.dev
164
172
  source_code_uri: https://github.com/aikidosec/firewall-ruby
165
173
  changelog_uri: https://github.com/aikidosec/firewall-ruby/blob/main/CHANGELOG.md
166
- post_install_message:
174
+ post_install_message:
167
175
  rdoc_options: []
168
176
  require_paths:
169
177
  - lib
@@ -179,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
187
  version: '0'
180
188
  requirements: []
181
189
  rubygems_version: 3.5.22
182
- signing_key:
190
+ signing_key:
183
191
  specification_version: 4
184
192
  summary: Embedded Web Application Firewall that autonomously protects Ruby apps against
185
193
  common and critical attacks.