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.
- 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/libzen-v0.1.37.x86_64.so +0 -0
- 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 +16 -8
- 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
|
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: x86_64-linux
|
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
|
@@ -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.
|
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.
|
Binary file
|