aikido-zen 0.2.0-x86_64-linux → 1.0.1.beta.2-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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.aikido +6 -0
  3. data/.simplecov +6 -0
  4. data/README.md +67 -83
  5. data/benchmarks/README.md +8 -12
  6. data/docs/rails.md +1 -1
  7. data/lib/aikido/zen/agent.rb +10 -8
  8. data/lib/aikido/zen/api_client.rb +14 -4
  9. data/lib/aikido/zen/background_worker.rb +52 -0
  10. data/lib/aikido/zen/collector.rb +12 -1
  11. data/lib/aikido/zen/config.rb +20 -0
  12. data/lib/aikido/zen/context.rb +4 -0
  13. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  14. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  15. data/lib/aikido/zen/detached_agent/server.rb +41 -0
  16. data/lib/aikido/zen/detached_agent.rb +2 -0
  17. data/lib/aikido/zen/errors.rb +8 -0
  18. data/lib/aikido/zen/internals.rb +41 -7
  19. data/lib/aikido/zen/libzen-v0.1.39-x86_64-linux.so +0 -0
  20. data/lib/aikido/zen/middleware/rack_throttler.rb +9 -3
  21. data/lib/aikido/zen/middleware/request_tracker.rb +6 -4
  22. data/lib/aikido/zen/outbound_connection_monitor.rb +4 -0
  23. data/lib/aikido/zen/rails_engine.rb +8 -8
  24. data/lib/aikido/zen/rate_limiter/breaker.rb +3 -3
  25. data/lib/aikido/zen/rate_limiter.rb +6 -11
  26. data/lib/aikido/zen/request/heuristic_router.rb +6 -0
  27. data/lib/aikido/zen/request/rails_router.rb +6 -18
  28. data/lib/aikido/zen/request/schema/auth_schemas.rb +14 -0
  29. data/lib/aikido/zen/request/schema.rb +18 -0
  30. data/lib/aikido/zen/runtime_settings.rb +2 -2
  31. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +4 -2
  32. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +4 -2
  33. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +4 -2
  34. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +33 -21
  35. data/lib/aikido/zen/scanners/ssrf_scanner.rb +6 -1
  36. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +6 -0
  37. data/lib/aikido/zen/sink.rb +11 -1
  38. data/lib/aikido/zen/sinks/action_controller.rb +9 -4
  39. data/lib/aikido/zen/sinks/async_http.rb +35 -16
  40. data/lib/aikido/zen/sinks/curb.rb +52 -26
  41. data/lib/aikido/zen/sinks/em_http.rb +39 -25
  42. data/lib/aikido/zen/sinks/excon.rb +63 -45
  43. data/lib/aikido/zen/sinks/file.rb +67 -71
  44. data/lib/aikido/zen/sinks/http.rb +38 -19
  45. data/lib/aikido/zen/sinks/httpclient.rb +51 -22
  46. data/lib/aikido/zen/sinks/httpx.rb +37 -18
  47. data/lib/aikido/zen/sinks/kernel.rb +18 -57
  48. data/lib/aikido/zen/sinks/mysql2.rb +19 -7
  49. data/lib/aikido/zen/sinks/net_http.rb +37 -19
  50. data/lib/aikido/zen/sinks/patron.rb +41 -24
  51. data/lib/aikido/zen/sinks/pg.rb +50 -27
  52. data/lib/aikido/zen/sinks/resolv.rb +37 -16
  53. data/lib/aikido/zen/sinks/socket.rb +46 -17
  54. data/lib/aikido/zen/sinks/sqlite3.rb +31 -12
  55. data/lib/aikido/zen/sinks/trilogy.rb +19 -7
  56. data/lib/aikido/zen/sinks.rb +29 -20
  57. data/lib/aikido/zen/sinks_dsl.rb +226 -0
  58. data/lib/aikido/zen/version.rb +2 -2
  59. data/lib/aikido/zen/worker.rb +5 -0
  60. data/lib/aikido/zen.rb +59 -9
  61. data/placeholder/.gitignore +4 -0
  62. data/placeholder/README.md +11 -0
  63. data/placeholder/Rakefile +75 -0
  64. data/placeholder/lib/placeholder.rb.template +3 -0
  65. data/placeholder/placeholder.gemspec.template +20 -0
  66. data/tasklib/bench.rake +29 -6
  67. data/tasklib/libzen.rake +70 -66
  68. data/tasklib/wrk.rb +88 -0
  69. metadata +23 -13
  70. data/CHANGELOG.md +0 -25
  71. data/lib/aikido/zen/libzen-v0.1.37.x86_64.so +0 -0
  72. data/lib/aikido.rb +0 -3
@@ -3,118 +3,114 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
5
  module File
6
- SINK = Sinks.add("File", scanners: [
7
- Aikido::Zen::Scanners::PathTraversalScanner
8
- ])
6
+ def self.load_sinks!
7
+ # Create a copy of the original method for internal use only to prevent
8
+ # recursion in PathTraversalScanner.
9
+ #
10
+ # IMPORTANT: The alias must be created before the method is overridden,
11
+ # when the extensions are prepended.
12
+ ::File.singleton_class.alias_method(:expand_path__internal_for_aikido_zen, :expand_path)
13
+
14
+ ::File.singleton_class.prepend(FileClassExtensions)
15
+ ::File.prepend(FileExtensions)
16
+ end
17
+
18
+ SINK = Sinks.add("File", scanners: [Scanners::PathTraversalScanner])
9
19
 
10
- module Extensions
11
- def self.scan_path(filepath, operation)
20
+ module Helpers
21
+ def self.scan(filepath, operation)
12
22
  SINK.scan(
13
23
  filepath: filepath,
14
24
  operation: operation
15
25
  )
16
26
  end
27
+ end
17
28
 
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
29
+ module FileClassExtensions
30
+ extend Sinks::DSL
25
31
 
26
- def open(filename, *, **)
27
- Extensions.scan_path(filename, "open")
28
- super
32
+ sink_before :open do |path|
33
+ Helpers.scan(path, "open")
29
34
  end
30
35
 
31
- def read(filename, *)
32
- Extensions.scan_path(filename, "read")
33
- super
36
+ sink_before :read do |path|
37
+ Helpers.scan(path, "read")
34
38
  end
35
39
 
36
- def write(filename, *, **)
37
- Extensions.scan_path(filename, "write")
38
- super
40
+ sink_before :write do |path|
41
+ Helpers.scan(path, "write")
39
42
  end
40
43
 
41
- def join(*)
42
- joined = super
43
- Extensions.scan_path(joined, "join")
44
- joined
44
+ sink_before :truncate do |file_name|
45
+ Helpers.scan(file_name, "truncate")
45
46
  end
46
47
 
47
- def chmod(mode, *paths)
48
- paths.each { |path| Extensions.scan_path(path, "chmod") }
49
- super
48
+ sink_before :rename do |old_name, new_name|
49
+ Helpers.scan(old_name, "rename")
50
+ Helpers.scan(new_name, "rename")
50
51
  end
51
52
 
52
- def chown(user, group, *paths)
53
- paths.each { |path| Extensions.scan_path(path, "chown") }
54
- super
53
+ sink_before :unlink do |*file_names|
54
+ file_names.each do |file_name|
55
+ Helpers.scan(file_name, "unlink")
56
+ end
55
57
  end
56
58
 
57
- def rename(from, to)
58
- Extensions.scan_path(from, "rename")
59
- Extensions.scan_path(to, "rename")
60
- super
59
+ sink_before :delete do |*file_names|
60
+ file_names.each do |file_name|
61
+ Helpers.scan(file_name, "delete")
62
+ end
61
63
  end
62
64
 
63
- def symlink(from, to)
64
- Extensions.scan_path(from, "symlink")
65
- Extensions.scan_path(to, "symlink")
66
- super
65
+ sink_before :symlink do |old_name, new_name|
66
+ Helpers.scan(old_name, "symlink")
67
+ Helpers.scan(new_name, "symlink")
67
68
  end
68
69
 
69
- def truncate(file_name, *)
70
- Extensions.scan_path(file_name, "truncate")
71
- super
70
+ sink_before :chmod do |_mode_int, *file_names|
71
+ file_names.each do |file_name|
72
+ Helpers.scan(file_name, "chmod")
73
+ end
72
74
  end
73
75
 
74
- def unlink(*args)
75
- args.each do |arg|
76
- Extensions.scan_path(arg, "unlink")
76
+ sink_before :chown do |_owner_int, group_int, *file_names|
77
+ file_names.each do |file_name|
78
+ Helpers.scan(file_name, "chown")
77
79
  end
78
- super
79
80
  end
80
81
 
81
- def delete(*args)
82
- args.each do |arg|
83
- Extensions.scan_path(arg, "delete")
82
+ sink_before :utime do |_atime, _mtime, *file_names|
83
+ file_names.each do |file_name|
84
+ Helpers.scan(file_name, "utime")
84
85
  end
85
- super
86
86
  end
87
87
 
88
- def utime(atime, mtime, *args)
89
- args.each do |arg|
90
- Extensions.scan_path(arg, "utime")
91
- end
92
- super
88
+ sink_after :join do |result|
89
+ Helpers.scan(result, "join")
90
+ end
91
+
92
+ sink_before :expand_path do |file_name|
93
+ Helpers.scan(file_name, "expand_path")
93
94
  end
94
95
 
95
- def expand_path(filename, *)
96
- Extensions.scan_path(filename, "expand_path")
97
- super
96
+ sink_before :realpath do |file_name|
97
+ Helpers.scan(file_name, "realpath")
98
98
  end
99
99
 
100
- def realpath(filename, *)
101
- Extensions.scan_path(filename, "realpath")
102
- super
100
+ sink_before :realdirpath do |file_name|
101
+ Helpers.scan(file_name, "realdirpath")
103
102
  end
103
+ end
104
+
105
+ module FileExtensions
106
+ extend Sinks::DSL
104
107
 
105
- def realdirpath(filename, *)
106
- Extensions.scan_path(filename, "realdirpath")
107
- super
108
+ sink_before :initialize do |path|
109
+ Helpers.scan(path, "new")
108
110
  end
109
111
  end
110
112
  end
111
113
  end
112
114
  end
113
115
 
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
116
+ Aikido::Zen::Sinks::File.load_sinks!
@@ -1,23 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../sink"
3
+ require_relative "../scanners/ssrf_scanner"
4
4
  require_relative "../outbound_connection_monitor"
5
5
 
6
6
  module Aikido::Zen
7
7
  module Sinks
8
8
  module HTTP
9
+ def self.load_sinks!
10
+ if Gem.loaded_specs["http"]
11
+ require "http"
12
+
13
+ ::HTTP::Client.prepend(ClientExtensions)
14
+ end
15
+ end
16
+
9
17
  SINK = Sinks.add("http", scanners: [
10
- Aikido::Zen::Scanners::SSRFScanner,
11
- Aikido::Zen::OutboundConnectionMonitor
18
+ Scanners::SSRFScanner,
19
+ OutboundConnectionMonitor
12
20
  ])
13
21
 
14
- module Extensions
22
+ module Helpers
15
23
  # Maps an HTTP Request to an Aikido OutboundConnection.
16
24
  #
17
25
  # @param req [HTTP::Request]
18
26
  # @return [Aikido::Zen::OutboundConnection]
19
27
  def self.build_outbound(req)
20
- Aikido::Zen::OutboundConnection.new(
28
+ OutboundConnection.new(
21
29
  host: req.socket_host,
22
30
  port: req.socket_port
23
31
  )
@@ -28,7 +36,7 @@ module Aikido::Zen
28
36
  # @param req [HTTP::Request]
29
37
  # @return [Aikido::Zen::Scanners::SSRFScanner::Request]
30
38
  def self.wrap_request(req)
31
- Aikido::Zen::Scanners::SSRFScanner::Request.new(
39
+ Scanners::SSRFScanner::Request.new(
32
40
  verb: req.verb,
33
41
  uri: URI(req.uri.to_s),
34
42
  headers: req.headers.to_h
@@ -36,32 +44,43 @@ module Aikido::Zen
36
44
  end
37
45
 
38
46
  def self.wrap_response(resp)
39
- Aikido::Zen::Scanners::SSRFScanner::Response.new(
47
+ Scanners::SSRFScanner::Response.new(
40
48
  status: resp.status,
41
49
  headers: resp.headers.to_h
42
50
  )
43
51
  end
44
52
 
45
- def perform(req, *)
46
- wrapped_request = Extensions.wrap_request(req)
53
+ def self.scan(request, connection, operation)
54
+ SINK.scan(
55
+ request: request,
56
+ connection: connection,
57
+ operation: operation
58
+ )
59
+ end
60
+ end
61
+
62
+ module ClientExtensions
63
+ extend Sinks::DSL
64
+
65
+ sink_around :perform do |super_call, req|
66
+ wrapped_request = Helpers.wrap_request(req)
47
67
 
48
68
  # Store the request information so the DNS sinks can pick it up.
49
- if (context = Aikido::Zen.current_context)
69
+ context = Aikido::Zen.current_context
70
+ if context
50
71
  prev_request = context["ssrf.request"]
51
72
  context["ssrf.request"] = wrapped_request
52
73
  end
53
74
 
54
- SINK.scan(
55
- request: wrapped_request,
56
- connection: Extensions.build_outbound(req),
57
- operation: "request"
58
- )
75
+ connection = Helpers.build_outbound(req)
76
+
77
+ Helpers.scan(wrapped_request, connection, "request")
59
78
 
60
- response = super
79
+ response = super_call.call
61
80
 
62
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
81
+ Scanners::SSRFScanner.track_redirects(
63
82
  request: wrapped_request,
64
- response: Extensions.wrap_response(response)
83
+ response: Helpers.wrap_response(response)
65
84
  )
66
85
 
67
86
  response
@@ -73,4 +92,4 @@ module Aikido::Zen
73
92
  end
74
93
  end
75
94
 
76
- ::HTTP::Client.prepend(Aikido::Zen::Sinks::HTTP::Extensions)
95
+ Aikido::Zen::Sinks::HTTP.load_sinks!
@@ -1,19 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../sink"
3
+ require_relative "../scanners/ssrf_scanner"
4
4
  require_relative "../outbound_connection_monitor"
5
5
 
6
6
  module Aikido::Zen
7
7
  module Sinks
8
8
  module HTTPClient
9
+ def self.load_sinks!
10
+ if Gem.loaded_specs["httpclient"]
11
+ require "httpclient"
12
+
13
+ ::HTTPClient.prepend(HTTPClient::HTTPClientExtensions)
14
+ end
15
+ end
16
+
9
17
  SINK = Sinks.add("httpclient", scanners: [
10
- Aikido::Zen::Scanners::SSRFScanner,
11
- Aikido::Zen::OutboundConnectionMonitor
18
+ Scanners::SSRFScanner,
19
+ OutboundConnectionMonitor
12
20
  ])
13
21
 
14
- module Extensions
22
+ module Helpers
15
23
  def self.wrap_request(req)
16
- Aikido::Zen::Scanners::SSRFScanner::Request.new(
24
+ Scanners::SSRFScanner::Request.new(
17
25
  verb: req.http_header.request_method,
18
26
  uri: req.http_header.request_uri,
19
27
  headers: req.headers
@@ -21,48 +29,69 @@ module Aikido::Zen
21
29
  end
22
30
 
23
31
  def self.wrap_response(resp)
24
- Aikido::Zen::Scanners::SSRFScanner::Response.new(
32
+ # Code coverage is disabled here because `do_get_header` is not called,
33
+ # because WebMock does not mock it.
34
+ # :nocov:
35
+ Scanners::SSRFScanner::Response.new(
25
36
  status: resp.http_header.status_code,
26
37
  headers: resp.headers
27
38
  )
39
+ # :nocov:
40
+ end
41
+
42
+ def self.scan(request, connection, operation)
43
+ SINK.scan(
44
+ request: request,
45
+ connection: connection,
46
+ operation: operation
47
+ )
28
48
  end
29
49
 
30
- def self.perform_scan(req, &block)
50
+ def self.sink(req, &block)
31
51
  wrapped_request = wrap_request(req)
32
- connection = Aikido::Zen::OutboundConnection.from_uri(req.http_header.request_uri)
52
+ connection = OutboundConnection.from_uri(req.http_header.request_uri)
33
53
 
34
54
  # Store the request information so the DNS sinks can pick it up.
35
- if (context = Aikido::Zen.current_context)
55
+ context = Aikido::Zen.current_context
56
+ if context
36
57
  prev_request = context["ssrf.request"]
37
58
  context["ssrf.request"] = wrapped_request
38
59
  end
39
60
 
40
- SINK.scan(connection: connection, request: wrapped_request, operation: "request")
61
+ scan(wrapped_request, connection, "request")
41
62
 
42
63
  yield
43
64
  ensure
44
65
  context["ssrf.request"] = prev_request if context
45
66
  end
67
+ end
68
+
69
+ module HTTPClientExtensions
70
+ extend Sinks::DSL
46
71
 
47
- def do_get_block(req, *)
48
- Extensions.perform_scan(req) { super }
72
+ private
73
+
74
+ sink_around :do_get_block do |super_call, req|
75
+ Helpers.sink(req, &super_call)
49
76
  end
50
77
 
51
- def do_get_stream(req, *)
52
- Extensions.perform_scan(req) { super }
78
+ sink_around :do_get_stream do |super_call, req|
79
+ Helpers.sink(req, &super_call)
53
80
  end
54
81
 
55
- def do_get_header(req, res, *)
56
- super.tap do
57
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
58
- request: Extensions.wrap_request(req),
59
- response: Extensions.wrap_response(res)
60
- )
61
- end
82
+ sink_after :do_get_header do |_result, req, res, _sess|
83
+ # Code coverage is disabled here because `do_get_header` is not called,
84
+ # because WebMock does not mock it.
85
+ # :nocov:
86
+ Scanners::SSRFScanner.track_redirects(
87
+ request: Helpers.wrap_request(req),
88
+ response: Helpers.wrap_response(res)
89
+ )
90
+ # :nocov:
62
91
  end
63
92
  end
64
93
  end
65
94
  end
66
95
  end
67
96
 
68
- ::HTTPClient.prepend(Aikido::Zen::Sinks::HTTPClient::Extensions)
97
+ Aikido::Zen::Sinks::HTTPClient.load_sinks!
@@ -1,19 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../sink"
3
+ require_relative "../scanners/ssrf_scanner"
4
4
  require_relative "../outbound_connection_monitor"
5
5
 
6
6
  module Aikido::Zen
7
7
  module Sinks
8
8
  module HTTPX
9
+ def self.load_sinks!
10
+ if Gem.loaded_specs["httpx"]
11
+ require "httpx"
12
+
13
+ ::HTTPX::Session.prepend(HTTPX::SessionExtensions)
14
+ end
15
+ end
16
+
9
17
  SINK = Sinks.add("httpx", scanners: [
10
- Aikido::Zen::Scanners::SSRFScanner,
11
- Aikido::Zen::OutboundConnectionMonitor
18
+ Scanners::SSRFScanner,
19
+ OutboundConnectionMonitor
12
20
  ])
13
21
 
14
- module Extensions
22
+ module Helpers
15
23
  def self.wrap_request(request)
16
- Aikido::Zen::Scanners::SSRFScanner::Request.new(
24
+ Scanners::SSRFScanner::Request.new(
17
25
  verb: request.verb,
18
26
  uri: request.uri,
19
27
  headers: request.headers.to_hash
@@ -21,35 +29,46 @@ module Aikido::Zen
21
29
  end
22
30
 
23
31
  def self.wrap_response(response)
24
- Aikido::Zen::Scanners::SSRFScanner::Response.new(
32
+ Scanners::SSRFScanner::Response.new(
25
33
  status: response.status,
26
34
  headers: response.headers.to_hash
27
35
  )
28
36
  end
29
37
 
30
- def send_request(request, *)
31
- wrapped_request = Extensions.wrap_request(request)
38
+ def self.scan(request, connection, operation)
39
+ SINK.scan(
40
+ request: request,
41
+ connection: connection,
42
+ operation: operation
43
+ )
44
+ end
45
+ end
46
+
47
+ module SessionExtensions
48
+ extend Sinks::DSL
49
+
50
+ sink_around :send_request do |super_call, request|
51
+ wrapped_request = Helpers.wrap_request(request)
32
52
 
33
53
  # Store the request information so the DNS sinks can pick it up.
34
- if (context = Aikido::Zen.current_context)
54
+ context = Aikido::Zen.current_context
55
+ if context
35
56
  prev_request = context["ssrf.request"]
36
57
  context["ssrf.request"] = wrapped_request
37
58
  end
38
59
 
39
- SINK.scan(
40
- connection: Aikido::Zen::OutboundConnection.from_uri(request.uri),
41
- request: wrapped_request,
42
- operation: "request"
43
- )
60
+ connection = OutboundConnection.from_uri(request.uri)
61
+
62
+ Helpers.scan(wrapped_request, connection, "request")
44
63
 
45
64
  request.on(:response) do |response|
46
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
65
+ Scanners::SSRFScanner.track_redirects(
47
66
  request: wrapped_request,
48
- response: Extensions.wrap_response(response)
67
+ response: Helpers.wrap_response(response)
49
68
  )
50
69
  end
51
70
 
52
- super
71
+ super_call.call
53
72
  ensure
54
73
  context["ssrf.request"] = prev_request if context
55
74
  end
@@ -58,4 +77,4 @@ module Aikido::Zen
58
77
  end
59
78
  end
60
79
 
61
- ::HTTPX::Session.prepend(Aikido::Zen::Sinks::HTTPX::Extensions)
80
+ Aikido::Zen::Sinks::HTTPX.load_sinks!
@@ -3,71 +3,32 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
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
6
+ def self.load_sinks!
7
+ ::Kernel.singleton_class.prepend(KernelExtensions)
8
+ ::Kernel.prepend(KernelExtensions)
9
+ end
27
10
 
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
11
+ SINK = Sinks.add("Kernel", scanners: [Scanners::ShellInjectionScanner])
52
12
 
53
- if args.size == 2 && args[0].is_a?(Hash)
54
- Extensions.scan_command(args[1], operation)
55
- end
13
+ module Helpers
14
+ def self.scan(command, operation)
15
+ SINK.scan(command: command, operation: operation)
56
16
  end
17
+ end
57
18
 
58
- def system(*args, **)
59
- send_arg_to_scan(args, "system")
60
- super
61
- end
19
+ module KernelExtensions
20
+ extend Sinks::DSL
62
21
 
63
- def spawn(*args, **)
64
- send_arg_to_scan(args, "spawn")
65
- super
22
+ %i[system spawn].each do |method_name|
23
+ sink_before method_name do |*args|
24
+ # Remove the optional environment argument before the command-line.
25
+ args.shift if args.first.is_a?(Hash)
26
+ Helpers.scan(args.first, method_name)
27
+ end
66
28
  end
67
29
  end
68
30
  end
69
31
  end
70
32
  end
71
33
 
72
- ::Kernel.singleton_class.prepend Aikido::Zen::Sinks::Kernel::Extensions
73
- ::Kernel.prepend Aikido::Zen::Sinks::Kernel::Extensions
34
+ Aikido::Zen::Sinks::Kernel.load_sinks!
@@ -1,21 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../sink"
4
-
5
3
  module Aikido::Zen
6
4
  module Sinks
7
5
  module Mysql2
6
+ def self.load_sinks!
7
+ if Gem.loaded_specs["mysql2"]
8
+ require "mysql2"
9
+
10
+ ::Mysql2::Client.prepend(ClientExtensions)
11
+ end
12
+ end
13
+
8
14
  SINK = Sinks.add("mysql2", scanners: [Scanners::SQLInjectionScanner])
9
15
 
10
- module Extensions
11
- def query(query, *)
12
- SINK.scan(query: query, dialect: :mysql, operation: "query")
16
+ module Helpers
17
+ def self.scan(query, operation)
18
+ SINK.scan(query: query, dialect: :mysql, operation: operation)
19
+ end
20
+ end
21
+
22
+ module ClientExtensions
23
+ extend Sinks::DSL
13
24
 
14
- super
25
+ sink_before :query do |sql|
26
+ Helpers.scan(sql, "query")
15
27
  end
16
28
  end
17
29
  end
18
30
  end
19
31
  end
20
32
 
21
- ::Mysql2::Client.prepend(Aikido::Zen::Sinks::Mysql2::Extensions)
33
+ Aikido::Zen::Sinks::Mysql2.load_sinks!