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
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Middleware
5
+ # Rack middleware used to track request
6
+ # It implements the logic under that which is considered worthy of being tracked.
7
+ class RequestTracker
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ request = Aikido::Zen::Middleware.request_from(env)
14
+ response = @app.call(env)
15
+
16
+ Aikido::Zen.track_request request
17
+
18
+ if Aikido::Zen.config.collect_api_schema? && request.route && track?(
19
+ status_code: response[0],
20
+ route: request.route.path,
21
+ http_method: request.request_method
22
+ )
23
+ Aikido::Zen.track_discovered_route(request)
24
+ end
25
+
26
+ response
27
+ end
28
+
29
+ IGNORED_METHODS = %w[OPTIONS HEAD]
30
+ IGNORED_EXTENSIONS = %w[properties config webmanifest]
31
+ IGNORED_SEGMENTS = ["cgi-bin"]
32
+ WELL_KNOWN_URIS = %w[
33
+ /.well-known/acme-challenge
34
+ /.well-known/amphtml
35
+ /.well-known/api-catalog
36
+ /.well-known/appspecific
37
+ /.well-known/ashrae
38
+ /.well-known/assetlinks.json
39
+ /.well-known/broadband-labels
40
+ /.well-known/brski
41
+ /.well-known/caldav
42
+ /.well-known/carddav
43
+ /.well-known/change-password
44
+ /.well-known/cmp
45
+ /.well-known/coap
46
+ /.well-known/coap-eap
47
+ /.well-known/core
48
+ /.well-known/csaf
49
+ /.well-known/csaf-aggregator
50
+ /.well-known/csvm
51
+ /.well-known/did.json
52
+ /.well-known/did-configuration.json
53
+ /.well-known/dnt
54
+ /.well-known/dnt-policy.txt
55
+ /.well-known/dots
56
+ /.well-known/ecips
57
+ /.well-known/edhoc
58
+ /.well-known/enterprise-network-security
59
+ /.well-known/enterprise-transport-security
60
+ /.well-known/est
61
+ /.well-known/genid
62
+ /.well-known/gnap-as-rs
63
+ /.well-known/gpc.json
64
+ /.well-known/gs1resolver
65
+ /.well-known/hoba
66
+ /.well-known/host-meta
67
+ /.well-known/host-meta.json
68
+ /.well-known/hosting-provider
69
+ /.well-known/http-opportunistic
70
+ /.well-known/idp-proxy
71
+ /.well-known/jmap
72
+ /.well-known/keybase.txt
73
+ /.well-known/knx
74
+ /.well-known/looking-glass
75
+ /.well-known/masque
76
+ /.well-known/matrix
77
+ /.well-known/mercure
78
+ /.well-known/mta-sts.txt
79
+ /.well-known/mud
80
+ /.well-known/nfv-oauth-server-configuration
81
+ /.well-known/ni
82
+ /.well-known/nodeinfo
83
+ /.well-known/nostr.json
84
+ /.well-known/oauth-authorization-server
85
+ /.well-known/oauth-protected-resource
86
+ /.well-known/ohttp-gateway
87
+ /.well-known/openid-federation
88
+ /.well-known/open-resource-discovery
89
+ /.well-known/openid-configuration
90
+ /.well-known/openorg
91
+ /.well-known/oslc
92
+ /.well-known/pki-validation
93
+ /.well-known/posh
94
+ /.well-known/privacy-sandbox-attestations.json
95
+ /.well-known/private-token-issuer-directory
96
+ /.well-known/probing.txt
97
+ /.well-known/pvd
98
+ /.well-known/rd
99
+ /.well-known/related-website-set.json
100
+ /.well-known/reload-config
101
+ /.well-known/repute-template
102
+ /.well-known/resourcesync
103
+ /.well-known/sbom
104
+ /.well-known/security.txt
105
+ /.well-known/ssf-configuration
106
+ /.well-known/sshfp
107
+ /.well-known/stun-key
108
+ /.well-known/terraform.json
109
+ /.well-known/thread
110
+ /.well-known/time
111
+ /.well-known/timezone
112
+ /.well-known/tdmrep.json
113
+ /.well-known/tor-relay
114
+ /.well-known/tpcd
115
+ /.well-known/traffic-advice
116
+ /.well-known/trust.txt
117
+ /.well-known/uma2-configuration
118
+ /.well-known/void
119
+ /.well-known/webfinger
120
+ /.well-known/webweaver.json
121
+ /.well-known/wot
122
+ ]
123
+
124
+ # @param status_code [Integer]
125
+ # @param route [String]
126
+ # @param http_method [String]
127
+ def track?(status_code:, route:, http_method:)
128
+ # In the UI we want to show only successful (2xx) or redirect (3xx) responses
129
+ # anything else is discarded.
130
+ return false unless status_code >= 200 && status_code <= 399
131
+
132
+ return false if IGNORED_METHODS.include?(http_method)
133
+
134
+ segments = route.split "/"
135
+
136
+ # Do not discover routes with dot files like `/path/to/.file` or `/.directory/file`
137
+ # We want to allow discovery of well-known URIs like `/.well-known/acme-challenge`
138
+ return false if segments.any? { |s| is_dot_file s } && !is_well_known_uri(route)
139
+
140
+ return false if segments.any? { |s| contains_ignored_string s }
141
+
142
+ # Check for every file segment if it contains a file extension and if it
143
+ # should be discovered or ignored
144
+ segments.all? { |s| should_track_extension s }
145
+ end
146
+
147
+ private
148
+
149
+ # Check if a path is a well-known URI
150
+ # e.g. /.well-known/acme-challenge
151
+ # https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml
152
+ def is_well_known_uri(route)
153
+ WELL_KNOWN_URIS.include?(route)
154
+ end
155
+
156
+ def is_dot_file(segment)
157
+ segment.start_with?(".") && segment.size > 1
158
+ end
159
+
160
+ def contains_ignored_string(segment)
161
+ IGNORED_SEGMENTS.any? { |ignored| segment.include?(ignored) }
162
+ end
163
+
164
+ # Ignore routes which contain file extensions
165
+ def should_track_extension(segment)
166
+ extension = get_file_extension(segment)
167
+
168
+ return true unless extension
169
+
170
+ # Do not discover files with extensions of 1 to 5 characters,
171
+ # e.g. file.css, file.js, file.woff2
172
+ return false if extension.size > 1 && extension.size < 6
173
+
174
+ # Ignore some file extensions that are longer than 5 characters or shorter than 2 chars
175
+ return false if IGNORED_EXTENSIONS.include?(extension)
176
+
177
+ true
178
+ end
179
+
180
+ def get_file_extension(segment)
181
+ extension = File.extname(segment)
182
+ if extension&.start_with?(".")
183
+ # Remove the dot from the extension
184
+ return extension[1..]
185
+ end
186
+ extension
187
+ end
188
+ end
189
+ end
190
+ end
@@ -15,10 +15,7 @@ module Aikido::Zen
15
15
  end
16
16
 
17
17
  def call(env)
18
- context = Context.from_rack_env(env)
19
-
20
- Aikido::Zen.current_context = env[ENV_KEY] = context
21
- Aikido::Zen.track_request(context.request)
18
+ Aikido::Zen.current_context = env[ENV_KEY] = Context.from_rack_env(env)
22
19
 
23
20
  @app.call(env)
24
21
  ensure
@@ -12,6 +12,8 @@ module Aikido::Zen
12
12
  @path = path
13
13
  end
14
14
 
15
+ UNKNOWN_PAYLOAD = Payload.new("unknown", "unknown", "unknown")
16
+
15
17
  alias_method :to_s, :value
16
18
 
17
19
  def ==(other)
@@ -14,6 +14,9 @@ module Aikido::Zen
14
14
 
15
15
  app.middleware.use Aikido::Zen::Middleware::SetContext
16
16
  app.middleware.use Aikido::Zen::Middleware::CheckAllowedAddresses
17
+ # Request Tracker stats do not consider failed request or 40x, so the middleware
18
+ # must be the last one wrapping the request.
19
+ app.middleware.use Aikido::Zen::Middleware::RequestTracker
17
20
 
18
21
  ActiveSupport.on_load(:action_controller) do
19
22
  # Due to how Rails sets up its middleware chain, the routing is evaluated
@@ -57,6 +60,11 @@ module Aikido::Zen
57
60
  # that any gems required after aikido-zen are detected and patched
58
61
  # accordingly.
59
62
  Aikido::Zen.load_sinks!
63
+
64
+ # Agent's bootstrap process has finished —Controllers are patched to block
65
+ # unwanted requests, sinks are loaded, scanners are running—, so we mark
66
+ # the agent as installed.
67
+ Aikido::Zen.middleware_installed!
60
68
  end
61
69
  end
62
70
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "synchronizable"
4
- require_relative "middleware/throttler"
4
+ require_relative "middleware/rack_throttler"
5
5
 
6
6
  module Aikido::Zen
7
7
  # Keeps track of all requests in this process, broken up by Route and further
@@ -68,8 +68,6 @@ module Aikido::Zen
68
68
  new(type: "boolean")
69
69
  when String
70
70
  new(type: "string")
71
- when Integer
72
- new(type: "integer")
73
71
  when Numeric
74
72
  new(type: "number")
75
73
  when Array
@@ -65,11 +65,6 @@ module Aikido::Zen
65
65
  in {type: _}, {type: "null"}
66
66
  new(definition.merge(optional: true))
67
67
 
68
- # number | integer => number
69
- in [{type: "integer"}, {type: "number"}] |
70
- [{type: "number"}, {type: "integer"}]
71
- new(definition.merge(other.definition).merge(type: "number"))
72
-
73
68
  # x | y => [x, y] if x != y
74
69
  else
75
70
  left_type, right_type = definition[:type], other.definition[:type]
@@ -56,9 +56,6 @@ module Aikido::Zen
56
56
  return self if other.nil?
57
57
 
58
58
  self.class.new(
59
- # TODO: this is currently overriding the content type with the new
60
- # value, but we should support APIs that accept input in many types
61
- # (e.g. JSON and XML)
62
59
  content_type: other.content_type,
63
60
  body_schema: body_schema.merge(other.body_schema),
64
61
  query_schema: query_schema.merge(other.query_schema),
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Scanners
5
+ module PathTraversal
6
+ DANGEROUS_PATH_PARTS = ["../", "..\\"]
7
+ LINUX_ROOT_FOLDERS = [
8
+ "/bin/",
9
+ "/boot/",
10
+ "/dev/",
11
+ "/etc/",
12
+ "/home/",
13
+ "/init/",
14
+ "/lib/",
15
+ "/media/",
16
+ "/mnt/",
17
+ "/opt/",
18
+ "/proc/",
19
+ "/root/",
20
+ "/run/",
21
+ "/sbin/",
22
+ "/srv/",
23
+ "/sys/",
24
+ "/tmp/",
25
+ "/usr/",
26
+ "/var/"
27
+ ]
28
+
29
+ DANGEROUS_PATH_STARTS = LINUX_ROOT_FOLDERS + ["c:/", "c:\\"]
30
+
31
+ module Helpers
32
+ def self.contains_unsafe_path_parts(filepath)
33
+ DANGEROUS_PATH_PARTS.each do |dangerous_part|
34
+ return true if filepath.include?(dangerous_part)
35
+ end
36
+
37
+ false
38
+ end
39
+
40
+ def self.starts_with_unsafe_path(filepath, user_input)
41
+ # Check if path is relative (not absolute or drive letter path)
42
+ # Required because `expand_path` will build absolute paths from relative paths
43
+ return false if Pathname.new(filepath).relative? || Pathname.new(user_input).relative?
44
+
45
+ normalized_path = File.expand_path__internal_for_aikido_zen(filepath).downcase
46
+ normalized_user_input = File.expand_path__internal_for_aikido_zen(user_input).downcase
47
+
48
+ DANGEROUS_PATH_STARTS.each do |dangerous_start|
49
+ if normalized_path.start_with?(dangerous_start) && normalized_path.start_with?(normalized_user_input)
50
+ # If the user input is the same as the dangerous start, we don't want to flag it
51
+ # to prevent false positives.
52
+ # e.g., if user input is /etc/ and the path is /etc/passwd, we don't want to flag it,
53
+ # as long as the user input does not contain a subdirectory or filename
54
+ if user_input == dangerous_start || user_input == dangerous_start.chomp("/")
55
+ return false
56
+ end
57
+ return true
58
+ end
59
+ end
60
+ false
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "path_traversal/helpers"
4
+
5
+ module Aikido::Zen
6
+ module Scanners
7
+ class PathTraversalScanner
8
+ # Checks if the user introduced input is trying to access other path using
9
+ # Path Traversal kind of attacks.
10
+ #
11
+ # @param filepath [String] the expanded path that is tried to be read
12
+ # @param context [Aikido::Zen::Context]
13
+ # @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
14
+ # @param operation [Symbol, String] name of the method being scanned.
15
+ #
16
+ # @return [Aikido::Zen::Attacks::PathTraversalAttack, nil] an Attack if any
17
+ # user input is detected to be attempting a Path Traversal Attack, or +nil+ if not.
18
+ def self.call(filepath:, sink:, context:, operation:)
19
+ return unless context
20
+
21
+ context.payloads.each do |payload|
22
+ next unless new(filepath, payload.value).attack?
23
+
24
+ return Attacks::PathTraversalAttack.new(
25
+ sink: sink,
26
+ input: payload,
27
+ filepath: filepath,
28
+ context: context,
29
+ operation: "#{sink.operation}.#{operation}"
30
+ )
31
+ end
32
+
33
+ nil
34
+ end
35
+
36
+ def initialize(filepath, input)
37
+ @filepath = filepath.downcase
38
+ @input = input.downcase
39
+ end
40
+
41
+ def attack?
42
+ # Single character are ignored because they don't pose a big threat
43
+ return false if @input.length <= 1
44
+
45
+ # We ignore cases where the user input is longer than the file path.
46
+ # Because the user input can't be part of the file path.
47
+ return false if @input.length > @filepath.length
48
+
49
+ # We ignore cases where the user input is not part of the file path.
50
+ return false unless @filepath.include?(@input)
51
+
52
+ if PathTraversal::Helpers.contains_unsafe_path_parts(@filepath) && PathTraversal::Helpers.contains_unsafe_path_parts(@input)
53
+ return true
54
+ end
55
+
56
+ # Check for absolute path traversal
57
+ PathTraversal::Helpers.starts_with_unsafe_path(@filepath, @input)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen::Scanners::ShellInjection
4
+ module Helpers
5
+ ESCAPE_CHARS = %W[' "]
6
+ DANGEROUS_CHARS_INSIDE_DOUBLE_QUOTES = %W[$ ` \\ !]
7
+ DANGEROUS_CHARS = [
8
+ "#", "!", '"', "$", "&", "'", "(", ")", "*", ";", "<", "=", ">", "?",
9
+ "[", "\\", "]", "^", "`", "{", "|", "}", " ", "\n", "\t", "~"
10
+ ]
11
+
12
+ COMMANDS = %w[sleep shutdown reboot poweroff halt ifconfig chmod chown ping
13
+ ssh scp curl wget telnet kill killall rm mv cp touch echo cat head
14
+ tail grep find awk sed sort uniq wc ls env ps who whoami id w df du
15
+ pwd uname hostname netstat passwd arch printenv logname pstree hostnamectl
16
+ set lsattr killall5 dmesg history free uptime finger top shopt :]
17
+
18
+ PATH_PREFIXES = %w[/bin/ /sbin/ /usr/bin/ /usr/sbin/ /usr/local/bin/ /usr/local/sbin/]
19
+
20
+ SEPARATORS = [" ", "\t", "\n", ";", "&", "|", "(", ")", "<", ">"]
21
+
22
+ # @param command [string]
23
+ # @param user_input [string]
24
+ def self.is_safely_encapsulated(command, user_input)
25
+ segments = command.split(user_input)
26
+
27
+ # The next condition is merely here to be compliant with what javascript does when splitting strings:
28
+ # From js doc https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
29
+ # > If separator appears at the beginning (or end) of the string, it still has the effect of splitting,
30
+ # > resulting in an empty (i.e. zero length) string appearing at the first (or last) position of
31
+ # > the returned array.
32
+ # This is necessary because this code is ported form the firewall-node code.
33
+ if user_input.length > 1
34
+ if command.start_with? user_input
35
+ segments.unshift ""
36
+ end
37
+
38
+ if command.end_with? user_input
39
+ segments << ""
40
+ end
41
+ end
42
+
43
+ # Call the helper function to get current and next segments
44
+ get_current_and_next_segments(segments).all? do |segments_pair|
45
+ char_before_user_input = segments_pair[:current_segment][-1]
46
+ char_after_user_input = segments_pair[:next_segment][0]
47
+
48
+ # Check if the character before is an escape character
49
+ is_escape_char = ESCAPE_CHARS.include?(char_before_user_input)
50
+
51
+ unless is_escape_char
52
+ next false
53
+ end
54
+
55
+ # If characters before and after the user input do not match, return false
56
+ next false if char_before_user_input != char_after_user_input
57
+
58
+ # If user input contains the escape character, return false
59
+ next false if user_input.include?(char_before_user_input)
60
+
61
+ # Handle dangerous characters inside double quotes
62
+ if char_before_user_input == '"' && DANGEROUS_CHARS_INSIDE_DOUBLE_QUOTES.any? { |char| user_input.include?(char) }
63
+ next false
64
+ end
65
+
66
+ next true
67
+ end
68
+ end
69
+
70
+ def self.get_current_and_next_segments(segments)
71
+ segments.each_cons(2).map { |current_segment, next_segment| {current_segment: current_segment, next_segment: next_segment} }
72
+ end
73
+
74
+ # Helper function for sorting commands by length (longer commands first)
75
+ def self.by_length(a, b)
76
+ b.length - a.length
77
+ end
78
+
79
+ # Escape characters with special meaning either inside or outside character sets.
80
+ # Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler
81
+ # form would be disallowed by Unicode patterns’ stricter grammar.
82
+ #
83
+ # Inspired by https://github.com/sindresorhus/escape-string-regexp/
84
+ def self.escape_string_regexp(string)
85
+ string.gsub(/[|\\{}()\[\]^$+*?.]/) { "\\#{$&}" }.gsub("-", '\\x2d')
86
+ end
87
+
88
+ # Construct the regex for commands
89
+ COMMANDS_REGEX = Regexp.new(
90
+ "([/.]*((#{PATH_PREFIXES.map { |p| Helpers.escape_string_regexp(p) }.join("|")})?((#{COMMANDS.sort(&method(:by_length)).join("|")}))))",
91
+ Regexp::IGNORECASE
92
+ )
93
+
94
+ def self.contains_shell_syntax(command, user_input)
95
+ # Check if input is only whitespace
96
+ return false if user_input.strip.empty?
97
+
98
+ # Check if the user input contains any dangerous characters
99
+ if DANGEROUS_CHARS.any? { |c| user_input.include?(c) }
100
+ return true
101
+ end
102
+
103
+ # If the command is exactly the same as the user input, check if it matches the regex
104
+ if command == user_input
105
+ return match_all(command, COMMANDS_REGEX).any? do |match|
106
+ match[:match].length == command.length && match[:match] == command
107
+ end
108
+ end
109
+
110
+ # Check if the command contains a commonly used command
111
+ match_all(command, COMMANDS_REGEX).each do |match|
112
+ # We found a command like `rm` or `/sbin/shutdown` in the command
113
+ # Check if the command is the same as the user input
114
+ # If it's not the same, continue searching
115
+ next if user_input != match[:match]
116
+
117
+ # Otherwise, we'll check if the command is surrounded by separators
118
+ # These separators are used to separate commands and arguments
119
+ # e.g. `rm<space>-rf`
120
+ # e.g. `ls<newline>whoami`
121
+ # e.g. `echo<tab>hello` Check if the command is surrounded by separators
122
+ char_before = if match[:index] - 1 < 0
123
+ nil
124
+ else
125
+ command[match[:index] - 1]
126
+ end
127
+
128
+ char_after = if match[:index] + match[:match].length >= command.length
129
+ nil
130
+ else
131
+ command[match[:index] + match[:match].length]
132
+ end
133
+
134
+ # e.g. `<separator>rm<separator>`
135
+ if SEPARATORS.include?(char_before) && SEPARATORS.include?(char_after)
136
+ return true
137
+ end
138
+
139
+ # e.g. `<separator>rm`
140
+ if SEPARATORS.include?(char_before) && char_after.nil?
141
+ return true
142
+ end
143
+
144
+ # e.g. `rm<separator>`
145
+ if char_before.nil? && SEPARATORS.include?(char_after)
146
+ return true
147
+ end
148
+ end
149
+
150
+ false
151
+ end
152
+
153
+ def self.match_all(string, regex)
154
+ string.enum_for(:scan, regex).map do |match|
155
+ {match: match[0], index: $~.begin(0)}
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shell_injection/helpers"
4
+
5
+ module Aikido::Zen
6
+ module Scanners
7
+ class ShellInjectionScanner
8
+ # @param command [String]
9
+ # @param sink [Aikido::Zen::Sink]
10
+ # @param context [Aikido::Zen::Context]
11
+ # @param operation [Symbol, String]
12
+ #
13
+ def self.call(command:, sink:, context:, operation:)
14
+ return unless context
15
+
16
+ context.payloads.each do |payload|
17
+ next unless new(command, payload.value).attack?
18
+
19
+ return Attacks::ShellInjectionAttack.new(
20
+ sink: sink,
21
+ input: payload,
22
+ command: command,
23
+ context: context,
24
+ operation: "#{sink.operation}.#{operation}"
25
+ )
26
+ end
27
+
28
+ nil
29
+ end
30
+
31
+ # @param command [String]
32
+ # @param input [String]
33
+ def initialize(command, input)
34
+ @command = command
35
+ @input = input
36
+ end
37
+
38
+ def attack?
39
+ # Block single ~ character. For example `echo ~`
40
+ if @input == "~"
41
+ if @command.size > 1 && @command.include?("~")
42
+ return true
43
+ end
44
+ end
45
+
46
+ # we ignore single character since they don't pose a big threat.
47
+ # They are only able to crash the shell, not execute arbitraty commands.
48
+ return false if @input.size <= 1
49
+
50
+ # We ignore cases where the user input is longer than the command because
51
+ # the user input can't be part of the command
52
+ return false if @input.size > @command.size
53
+
54
+ return false unless @command.include?(@input)
55
+
56
+ return false if ShellInjection::Helpers.is_safely_encapsulated @command, @input
57
+
58
+ ShellInjection::Helpers.contains_shell_syntax @command, @input
59
+ end
60
+ end
61
+ end
62
+ end
@@ -22,10 +22,6 @@ module Aikido::Zen
22
22
  # @raise [Aikido::Zen::InternalsError] if an error occurs when loading or
23
23
  # calling zenlib. See Sink#scan.
24
24
  def self.call(query:, dialect:, sink:, context:, operation:)
25
- # FIXME: This assumes queries executed outside of an HTTP request are
26
- # safe, but this is not the case. For example, if an HTTP request
27
- # enqueues a background job, passing user input verbatim, the job might
28
- # pass that input to a query without having a current request in scope.
29
25
  return if context.nil?
30
26
 
31
27
  dialect = DIALECTS.fetch(dialect) do
@@ -228,11 +228,11 @@ module Aikido::Zen
228
228
  # @api private
229
229
  class RedirectChains
230
230
  def initialize
231
- @redirects = {}
231
+ @redirects = Hash.new { |h, k| h[k] = [] }
232
232
  end
233
233
 
234
234
  def add(source:, destination:)
235
- @redirects[destination] = source
235
+ @redirects[destination].push(source)
236
236
  self
237
237
  end
238
238
 
@@ -242,11 +242,14 @@ module Aikido::Zen
242
242
  #
243
243
  # @param uri [URI]
244
244
  # @return [URI, nil]
245
- def origin(uri)
246
- source = @redirects[uri]
245
+ def origin(uri, visited = Set.new)
246
+ source = @redirects[uri].first
247
247
 
248
- if @redirects[source]
249
- origin(source)
248
+ return source if visited.include?(source)
249
+ visited << source
250
+
251
+ if !@redirects[source].empty?
252
+ origin(source, visited)
250
253
  else
251
254
  source
252
255
  end
@@ -3,3 +3,5 @@
3
3
  require_relative "scanners/sql_injection_scanner"
4
4
  require_relative "scanners/stored_ssrf_scanner"
5
5
  require_relative "scanners/ssrf_scanner"
6
+ require_relative "scanners/path_traversal_scanner"
7
+ require_relative "scanners/shell_injection_scanner"