aikido-zen 1.0.0.pre.beta.1-x86_64-darwin → 1.0.1.beta.2-x86_64-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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.aikido +6 -0
  3. data/README.md +67 -83
  4. data/lib/aikido/zen/config.rb +11 -2
  5. data/lib/aikido/zen/context.rb +4 -0
  6. data/lib/aikido/zen/internals.rb +41 -7
  7. data/lib/aikido/zen/libzen-v0.1.39-x86_64-darwin.dylib +0 -0
  8. data/lib/aikido/zen/middleware/request_tracker.rb +6 -4
  9. data/lib/aikido/zen/rails_engine.rb +5 -9
  10. data/lib/aikido/zen/request/heuristic_router.rb +6 -0
  11. data/lib/aikido/zen/sink.rb +5 -0
  12. data/lib/aikido/zen/sinks/async_http.rb +35 -16
  13. data/lib/aikido/zen/sinks/curb.rb +52 -26
  14. data/lib/aikido/zen/sinks/em_http.rb +39 -25
  15. data/lib/aikido/zen/sinks/excon.rb +63 -45
  16. data/lib/aikido/zen/sinks/file.rb +67 -71
  17. data/lib/aikido/zen/sinks/http.rb +38 -19
  18. data/lib/aikido/zen/sinks/httpclient.rb +51 -22
  19. data/lib/aikido/zen/sinks/httpx.rb +37 -18
  20. data/lib/aikido/zen/sinks/kernel.rb +18 -57
  21. data/lib/aikido/zen/sinks/mysql2.rb +19 -7
  22. data/lib/aikido/zen/sinks/net_http.rb +37 -19
  23. data/lib/aikido/zen/sinks/patron.rb +41 -24
  24. data/lib/aikido/zen/sinks/pg.rb +50 -27
  25. data/lib/aikido/zen/sinks/resolv.rb +37 -16
  26. data/lib/aikido/zen/sinks/socket.rb +33 -17
  27. data/lib/aikido/zen/sinks/sqlite3.rb +31 -12
  28. data/lib/aikido/zen/sinks/trilogy.rb +19 -7
  29. data/lib/aikido/zen/sinks.rb +29 -20
  30. data/lib/aikido/zen/sinks_dsl.rb +226 -0
  31. data/lib/aikido/zen/version.rb +2 -2
  32. data/lib/aikido/zen.rb +18 -1
  33. data/placeholder/.gitignore +4 -0
  34. data/placeholder/README.md +11 -0
  35. data/placeholder/Rakefile +75 -0
  36. data/placeholder/lib/placeholder.rb.template +3 -0
  37. data/placeholder/placeholder.gemspec.template +20 -0
  38. data/tasklib/libzen.rake +70 -66
  39. metadata +17 -13
  40. data/CHANGELOG.md +0 -25
  41. data/lib/aikido/zen/libzen-v0.1.37.x86_64.dylib +0 -0
  42. data/lib/aikido.rb +0 -3
@@ -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!
@@ -1,25 +1,32 @@
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 Net
9
9
  module HTTP
10
+ def self.load_sinks!
11
+ # In stdlib but not always required
12
+ require "net/http"
13
+
14
+ ::Net::HTTP.prepend(Net::HTTP::HTTPExtensions)
15
+ end
16
+
10
17
  SINK = Sinks.add("net-http", scanners: [
11
- Aikido::Zen::Scanners::SSRFScanner,
12
- Aikido::Zen::OutboundConnectionMonitor
18
+ Scanners::SSRFScanner,
19
+ OutboundConnectionMonitor
13
20
  ])
14
21
 
15
- module Extensions
22
+ module Helpers
16
23
  # Maps a Net::HTTP connection to an Aikido OutboundConnection,
17
24
  # which our tooling expects.
18
25
  #
19
26
  # @param http [Net::HTTP]
20
27
  # @return [Aikido::Zen::OutboundConnection]
21
28
  def self.build_outbound(http)
22
- Aikido::Zen::OutboundConnection.new(
29
+ OutboundConnection.new(
23
30
  host: http.address,
24
31
  port: http.port
25
32
  )
@@ -34,7 +41,7 @@ module Aikido::Zen
34
41
  path: req.path
35
42
  }))
36
43
 
37
- Aikido::Zen::Scanners::SSRFScanner::Request.new(
44
+ Scanners::SSRFScanner::Request.new(
38
45
  verb: req.method,
39
46
  uri: uri,
40
47
  headers: req.to_hash,
@@ -43,33 +50,44 @@ module Aikido::Zen
43
50
  end
44
51
 
45
52
  def self.wrap_response(response)
46
- Aikido::Zen::Scanners::SSRFScanner::Response.new(
53
+ Scanners::SSRFScanner::Response.new(
47
54
  status: response.code.to_i,
48
55
  headers: response.to_hash,
49
56
  header_normalizer: ->(val) { Array(val).join(", ") }
50
57
  )
51
58
  end
52
59
 
53
- def request(req, *)
54
- wrapped_request = Extensions.wrap_request(req, self)
60
+ def self.scan(request, connection, operation)
61
+ SINK.scan(
62
+ request: request,
63
+ connection: connection,
64
+ operation: operation
65
+ )
66
+ end
67
+ end
68
+
69
+ module HTTPExtensions
70
+ extend Sinks::DSL
71
+
72
+ sink_around :request do |super_call, req|
73
+ wrapped_request = Helpers.wrap_request(req, self)
55
74
 
56
75
  # Store the request information so the DNS sinks can pick it up.
57
- if (context = Aikido::Zen.current_context)
76
+ context = Aikido::Zen.current_context
77
+ if context
58
78
  prev_request = context["ssrf.request"]
59
79
  context["ssrf.request"] = wrapped_request
60
80
  end
61
81
 
62
- SINK.scan(
63
- connection: Extensions.build_outbound(self),
64
- request: wrapped_request,
65
- operation: "request"
66
- )
82
+ connection = Helpers.build_outbound(self)
83
+
84
+ Helpers.scan(wrapped_request, connection, "request")
67
85
 
68
- response = super
86
+ response = super_call.call
69
87
 
70
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
88
+ Scanners::SSRFScanner.track_redirects(
71
89
  request: wrapped_request,
72
- response: Extensions.wrap_response(response)
90
+ response: Helpers.wrap_response(response)
73
91
  )
74
92
 
75
93
  response
@@ -82,4 +100,4 @@ module Aikido::Zen
82
100
  end
83
101
  end
84
102
 
85
- ::Net::HTTP.prepend(Aikido::Zen::Sinks::Net::HTTP::Extensions)
103
+ Aikido::Zen::Sinks::Net::HTTP.load_sinks!
@@ -1,56 +1,75 @@
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 Patron
9
+ def self.load_sinks!
10
+ if Gem.loaded_specs["patron"]
11
+ require "patron"
12
+
13
+ ::Patron::Session.prepend(SessionExtensions)
14
+ end
15
+ end
16
+
9
17
  SINK = Sinks.add("patron", 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_response(request, response)
16
24
  # In this case, automatic redirection happened inside libcurl.
17
25
  if response.url != request.url && !response.url.to_s.empty?
18
- Aikido::Zen::Scanners::SSRFScanner::Response.new(
26
+ Scanners::SSRFScanner::Response.new(
19
27
  status: 302, # We can't know what the actual status was, but we just need a 3XX
20
28
  headers: response.headers.merge("Location" => response.url)
21
29
  )
22
30
  else
23
- Aikido::Zen::Scanners::SSRFScanner::Response.new(
31
+ Scanners::SSRFScanner::Response.new(
24
32
  status: response.status,
25
33
  headers: response.headers
26
34
  )
27
35
  end
28
36
  end
29
37
 
30
- def handle_request(request)
31
- wrapped_request = Aikido::Zen::Scanners::SSRFScanner::Request.new(
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 :handle_request do |super_call, request|
51
+ wrapped_request = Scanners::SSRFScanner::Request.new(
32
52
  verb: request.action,
33
53
  uri: URI(request.url),
34
54
  headers: request.headers
35
55
  )
36
56
 
37
57
  # Store the request information so the DNS sinks can pick it up.
38
- if (context = Aikido::Zen.current_context)
58
+ context = Aikido::Zen.current_context
59
+ if context
39
60
  prev_request = context["ssrf.request"]
40
61
  context["ssrf.request"] = wrapped_request
41
62
  end
42
63
 
43
- SINK.scan(
44
- connection: Aikido::Zen::OutboundConnection.from_uri(URI(request.url)),
45
- request: wrapped_request,
46
- operation: "request"
47
- )
64
+ connection = OutboundConnection.from_uri(URI(request.url))
65
+
66
+ Helpers.scan(wrapped_request, connection, "request")
48
67
 
49
- response = super
68
+ response = super_call.call
50
69
 
51
- Aikido::Zen::Scanners::SSRFScanner.track_redirects(
70
+ Scanners::SSRFScanner.track_redirects(
52
71
  request: wrapped_request,
53
- response: Extensions.wrap_response(request, response)
72
+ response: Helpers.wrap_response(request, response)
54
73
  )
55
74
 
56
75
  # When libcurl has follow_location set, it will handle redirections
@@ -62,18 +81,16 @@ module Aikido::Zen
62
81
  # stop the response from being exposed to the user. This downgrades
63
82
  # the SSRF into a blind SSRF, which is better than doing nothing.
64
83
  if request.url != response.url && !response.url.to_s.empty?
65
- last_effective_request = Aikido::Zen::Scanners::SSRFScanner::Request.new(
84
+ last_effective_request = Scanners::SSRFScanner::Request.new(
66
85
  verb: request.action,
67
86
  uri: URI(response.url),
68
87
  headers: request.headers
69
88
  )
70
89
  context["ssrf.request"] = last_effective_request if context
71
90
 
72
- SINK.scan(
73
- connection: Aikido::Zen::OutboundConnection.from_uri(URI(response.url)),
74
- request: last_effective_request,
75
- operation: "request"
76
- )
91
+ connection = OutboundConnection.from_uri(URI(response.url))
92
+
93
+ Helpers.scan(last_effective_request, connection, "request")
77
94
  end
78
95
 
79
96
  response
@@ -85,4 +102,4 @@ module Aikido::Zen
85
102
  end
86
103
  end
87
104
 
88
- ::Patron::Session.prepend(Aikido::Zen::Sinks::Patron::Extensions)
105
+ Aikido::Zen::Sinks::Patron.load_sinks!
@@ -1,51 +1,74 @@
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 PG
6
+ def self.load_sinks!
7
+ if Gem.loaded_specs["pg"]
8
+ require "pg"
9
+
10
+ ::PG::Connection.prepend(PG::ConnectionExtensions)
11
+ end
12
+ end
13
+
8
14
  SINK = Sinks.add("pg", scanners: [Scanners::SQLInjectionScanner])
9
15
 
10
- # For some reason, the ActiveRecord pg adapter does not wrap exceptions in
11
- # StatementInvalid, which leads to inconsistent handling. This guarantees
12
- # that all Zen errors are wrapped in a StatementInvalid, so documentation
13
- # can be consistent.
14
- WRAP_EXCEPTIONS = if defined?(ActiveRecord::StatementInvalid)
15
- <<~RUBY
16
- rescue Aikido::Zen::SQLInjectionError
17
- raise ActiveRecord::StatementInvalid
18
- RUBY
16
+ module Helpers
17
+ # For some reason, the ActiveRecord pg adaptor does not wrap exceptions
18
+ # in ActiveRecord::StatementInvalid, leading to inconsistent handling.
19
+ # This guarantees that Aikido::Zen::SQLInjectionErrors are wrapped in
20
+ # an ActiveRecord::StatementInvalid.
21
+ def self.safe(&block)
22
+ # Code coverage is disabled here because this ActiveRecord behavior is
23
+ # exercised in end-to-end tests, which are not covered by SimpleCov.
24
+ # :nocov:
25
+ if !defined?(ActiveRecord::StatementInvalid)
26
+ Sinks::DSL.safe(&block)
27
+ else
28
+ begin
29
+ Sinks::DSL.safe(&block)
30
+ rescue Aikido::Zen::SQLInjectionError => err
31
+ raise ActiveRecord::StatementInvalid, cause: err
32
+ end
33
+ end
34
+ # :nocov:
35
+ end
36
+
37
+ def self.scan(query, operation)
38
+ SINK.scan(
39
+ query: query,
40
+ dialect: :postgresql,
41
+ operation: operation
42
+ )
43
+ end
19
44
  end
20
45
 
21
- module Extensions
46
+ module ConnectionExtensions
47
+ extend Sinks::DSL
48
+
22
49
  %i[
23
50
  send_query exec sync_exec async_exec
24
51
  send_query_params exec_params sync_exec_params async_exec_params
25
- ].each do |method|
26
- module_eval <<~RUBY, __FILE__, __LINE__ + 1
27
- def #{method}(query, *)
28
- SINK.scan(query: query, dialect: :postgresql, operation: :#{method})
29
- super
30
- #{WRAP_EXCEPTIONS}
52
+ ].each do |method_name|
53
+ presafe_sink_before method_name do |query|
54
+ Helpers.safe do
55
+ Helpers.scan(query, method_name)
31
56
  end
32
- RUBY
57
+ end
33
58
  end
34
59
 
35
60
  %i[
36
61
  send_prepare prepare async_prepare sync_prepare
37
- ].each do |method|
38
- module_eval <<~RUBY, __FILE__, __LINE__ + 1
39
- def #{method}(_, query, *)
40
- SINK.scan(query: query, dialect: :postgresql, operation: :#{method})
41
- super
42
- #{WRAP_EXCEPTIONS}
62
+ ].each do |method_name|
63
+ presafe_sink_before method_name do |_, query|
64
+ Helpers.safe do
65
+ Helpers.scan(query, method_name)
43
66
  end
44
- RUBY
67
+ end
45
68
  end
46
69
  end
47
70
  end
48
71
  end
49
72
  end
50
73
 
51
- ::PG::Connection.prepend(Aikido::Zen::Sinks::PG::Extensions)
74
+ Aikido::Zen::Sinks::PG.load_sinks!