aikido-zen 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.simplecov +1 -0
- data/CHANGELOG.md +4 -0
- data/README.md +11 -2
- data/benchmarks/rails7.1_sql_injection.js +30 -34
- data/docs/banner.svg +128 -129
- data/docs/config.md +8 -6
- data/docs/rails.md +2 -2
- data/lib/aikido/zen/agent.rb +3 -1
- data/lib/aikido/zen/api_client.rb +3 -3
- data/lib/aikido/zen/attack.rb +105 -36
- data/lib/aikido/zen/collector/routes.rb +2 -0
- data/lib/aikido/zen/collector.rb +19 -3
- data/lib/aikido/zen/config.rb +44 -20
- data/lib/aikido/zen/errors.rb +10 -1
- data/lib/aikido/zen/event.rb +4 -2
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +2 -14
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/{throttler.rb → rack_throttler.rb} +3 -11
- data/lib/aikido/zen/middleware/request_tracker.rb +190 -0
- data/lib/aikido/zen/middleware/set_context.rb +1 -4
- data/lib/aikido/zen/payload.rb +2 -0
- data/lib/aikido/zen/rails_engine.rb +8 -0
- data/lib/aikido/zen/rate_limiter.rb +1 -1
- data/lib/aikido/zen/request/schema/builder.rb +0 -2
- data/lib/aikido/zen/request/schema/definition.rb +0 -5
- data/lib/aikido/zen/request/schema.rb +0 -3
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +61 -0
- data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +62 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +0 -4
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +9 -6
- data/lib/aikido/zen/scanners.rb +2 -0
- data/lib/aikido/zen/sinks/action_controller.rb +26 -12
- data/lib/aikido/zen/sinks/file.rb +120 -0
- data/lib/aikido/zen/sinks/kernel.rb +73 -0
- data/lib/aikido/zen/sinks.rb +8 -0
- data/lib/aikido/zen/system_info.rb +1 -1
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen.rb +14 -1
- data/tasklib/bench.rake +3 -2
- metadata +15 -7
@@ -3,12 +3,12 @@
|
|
3
3
|
module Aikido::Zen
|
4
4
|
module Sinks
|
5
5
|
module ActionController
|
6
|
-
# Implements the "middleware" for
|
7
|
-
# need to check at the end of the `before_action`
|
8
|
-
# an actual Rack middleware, to allow for calls to
|
9
|
-
# made from before_actions in the host app, thus allowing
|
10
|
-
# by user ID rather than solely by IP.
|
11
|
-
class
|
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
|
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.
|
45
|
-
@
|
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
|
-
|
54
|
-
throttled = rate_limiter.throttle(self)
|
68
|
+
checker = Aikido::Zen::Sinks::ActionController.block_request_checker
|
55
69
|
|
56
|
-
yield if block_given? && !
|
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
|
data/lib/aikido/zen/sinks.rb
CHANGED
@@ -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)
|
data/lib/aikido/zen/version.rb
CHANGED
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: {"
|
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.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicolas Sanguinetti
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-03-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -52,7 +52,7 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
-
description:
|
55
|
+
description:
|
56
56
|
email:
|
57
57
|
- foca@foca.io
|
58
58
|
executables: []
|
@@ -94,8 +94,10 @@ files:
|
|
94
94
|
- lib/aikido/zen/event.rb
|
95
95
|
- lib/aikido/zen/internals.rb
|
96
96
|
- lib/aikido/zen/middleware/check_allowed_addresses.rb
|
97
|
+
- lib/aikido/zen/middleware/middleware.rb
|
98
|
+
- lib/aikido/zen/middleware/rack_throttler.rb
|
99
|
+
- lib/aikido/zen/middleware/request_tracker.rb
|
97
100
|
- lib/aikido/zen/middleware/set_context.rb
|
98
|
-
- lib/aikido/zen/middleware/throttler.rb
|
99
101
|
- lib/aikido/zen/outbound_connection.rb
|
100
102
|
- lib/aikido/zen/outbound_connection_monitor.rb
|
101
103
|
- lib/aikido/zen/package.rb
|
@@ -122,6 +124,10 @@ files:
|
|
122
124
|
- lib/aikido/zen/runtime_settings/rate_limit_settings.rb
|
123
125
|
- lib/aikido/zen/scan.rb
|
124
126
|
- lib/aikido/zen/scanners.rb
|
127
|
+
- lib/aikido/zen/scanners/path_traversal/helpers.rb
|
128
|
+
- lib/aikido/zen/scanners/path_traversal_scanner.rb
|
129
|
+
- lib/aikido/zen/scanners/shell_injection/helpers.rb
|
130
|
+
- lib/aikido/zen/scanners/shell_injection_scanner.rb
|
125
131
|
- lib/aikido/zen/scanners/sql_injection_scanner.rb
|
126
132
|
- lib/aikido/zen/scanners/ssrf/dns_lookups.rb
|
127
133
|
- lib/aikido/zen/scanners/ssrf/private_ip_checker.rb
|
@@ -134,9 +140,11 @@ files:
|
|
134
140
|
- lib/aikido/zen/sinks/curb.rb
|
135
141
|
- lib/aikido/zen/sinks/em_http.rb
|
136
142
|
- lib/aikido/zen/sinks/excon.rb
|
143
|
+
- lib/aikido/zen/sinks/file.rb
|
137
144
|
- lib/aikido/zen/sinks/http.rb
|
138
145
|
- lib/aikido/zen/sinks/httpclient.rb
|
139
146
|
- lib/aikido/zen/sinks/httpx.rb
|
147
|
+
- lib/aikido/zen/sinks/kernel.rb
|
140
148
|
- lib/aikido/zen/sinks/mysql2.rb
|
141
149
|
- lib/aikido/zen/sinks/net_http.rb
|
142
150
|
- lib/aikido/zen/sinks/patron.rb
|
@@ -159,7 +167,7 @@ metadata:
|
|
159
167
|
homepage_uri: https://aikido.dev
|
160
168
|
source_code_uri: https://github.com/aikidosec/firewall-ruby
|
161
169
|
changelog_uri: https://github.com/aikidosec/firewall-ruby/blob/main/CHANGELOG.md
|
162
|
-
post_install_message:
|
170
|
+
post_install_message:
|
163
171
|
rdoc_options: []
|
164
172
|
require_paths:
|
165
173
|
- lib
|
@@ -175,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
175
183
|
version: '0'
|
176
184
|
requirements: []
|
177
185
|
rubygems_version: 3.5.22
|
178
|
-
signing_key:
|
186
|
+
signing_key:
|
179
187
|
specification_version: 4
|
180
188
|
summary: Embedded Web Application Firewall that autonomously protects Ruby apps against
|
181
189
|
common and critical attacks.
|