ronin-vulns 0.1.0.beta1

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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.github/workflows/ruby.yml +31 -0
  4. data/.gitignore +13 -0
  5. data/.rspec +1 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +1 -0
  8. data/COPYING.txt +165 -0
  9. data/ChangeLog.md +22 -0
  10. data/Gemfile +34 -0
  11. data/README.md +328 -0
  12. data/Rakefile +34 -0
  13. data/bin/ronin-vulns +19 -0
  14. data/data/rfi_test.asp +21 -0
  15. data/data/rfi_test.aspx +25 -0
  16. data/data/rfi_test.cfm +27 -0
  17. data/data/rfi_test.jsp +19 -0
  18. data/data/rfi_test.php +24 -0
  19. data/data/rfi_test.pl +25 -0
  20. data/gemspec.yml +41 -0
  21. data/lib/ronin/vulns/cli/command.rb +39 -0
  22. data/lib/ronin/vulns/cli/commands/lfi.rb +145 -0
  23. data/lib/ronin/vulns/cli/commands/open_redirect.rb +119 -0
  24. data/lib/ronin/vulns/cli/commands/reflected_xss.rb +99 -0
  25. data/lib/ronin/vulns/cli/commands/rfi.rb +156 -0
  26. data/lib/ronin/vulns/cli/commands/scan.rb +316 -0
  27. data/lib/ronin/vulns/cli/commands/sqli.rb +133 -0
  28. data/lib/ronin/vulns/cli/commands/ssti.rb +126 -0
  29. data/lib/ronin/vulns/cli/logging.rb +78 -0
  30. data/lib/ronin/vulns/cli/web_vuln_command.rb +347 -0
  31. data/lib/ronin/vulns/cli.rb +45 -0
  32. data/lib/ronin/vulns/lfi/test_file.rb +91 -0
  33. data/lib/ronin/vulns/lfi.rb +266 -0
  34. data/lib/ronin/vulns/open_redirect.rb +118 -0
  35. data/lib/ronin/vulns/reflected_xss/context.rb +224 -0
  36. data/lib/ronin/vulns/reflected_xss/test_string.rb +149 -0
  37. data/lib/ronin/vulns/reflected_xss.rb +184 -0
  38. data/lib/ronin/vulns/rfi.rb +224 -0
  39. data/lib/ronin/vulns/root.rb +28 -0
  40. data/lib/ronin/vulns/sqli/error_pattern.rb +89 -0
  41. data/lib/ronin/vulns/sqli.rb +397 -0
  42. data/lib/ronin/vulns/ssti/test_expression.rb +104 -0
  43. data/lib/ronin/vulns/ssti.rb +203 -0
  44. data/lib/ronin/vulns/url_scanner.rb +218 -0
  45. data/lib/ronin/vulns/version.rb +26 -0
  46. data/lib/ronin/vulns/vuln.rb +49 -0
  47. data/lib/ronin/vulns/web_vuln/http_request.rb +223 -0
  48. data/lib/ronin/vulns/web_vuln.rb +774 -0
  49. data/man/ronin-vulns-lfi.1 +107 -0
  50. data/man/ronin-vulns-lfi.1.md +80 -0
  51. data/man/ronin-vulns-open-redirect.1 +98 -0
  52. data/man/ronin-vulns-open-redirect.1.md +73 -0
  53. data/man/ronin-vulns-reflected-xss.1 +95 -0
  54. data/man/ronin-vulns-reflected-xss.1.md +71 -0
  55. data/man/ronin-vulns-rfi.1 +107 -0
  56. data/man/ronin-vulns-rfi.1.md +80 -0
  57. data/man/ronin-vulns-scan.1 +138 -0
  58. data/man/ronin-vulns-scan.1.md +103 -0
  59. data/man/ronin-vulns-sqli.1 +107 -0
  60. data/man/ronin-vulns-sqli.1.md +80 -0
  61. data/man/ronin-vulns-ssti.1 +99 -0
  62. data/man/ronin-vulns-ssti.1.md +74 -0
  63. data/ronin-vulns.gemspec +60 -0
  64. metadata +161 -0
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-vulns - A Ruby library for blind vulnerability testing.
4
+ #
5
+ # Copyright (c) 2022 Hal Brodigan (postmodern.mod3 at gmail.com)
6
+ #
7
+ # ronin-vulns is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-vulns is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-vulns. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/vulns/web_vuln'
22
+ require 'ronin/vulns/lfi/test_file'
23
+
24
+ require 'ronin/support/text/patterns'
25
+ require 'ronin/support/crypto'
26
+ require 'ronin/support/compression'
27
+ require 'uri/query_params'
28
+ require 'base64'
29
+
30
+ module Ronin
31
+ module Vulns
32
+ #
33
+ # Represents a Local File Inclusion (LFI) vulnerability.
34
+ #
35
+ # ## Features
36
+ #
37
+ # * Supports UNIX and Windows paths.
38
+ # * Supports `%00` null terminator trick (fixed in PHP 5.3).
39
+ # * Supports Base64, ROT13, and Zlib `php://filter/`s.
40
+ #
41
+ class LFI < WebVuln
42
+
43
+ include Ronin::Support
44
+
45
+ # The test file for UNIX systems.
46
+ UNIX_TEST_FILE = TestFile.new('/etc/passwd', %r{(?:[a-z][a-z0-9_-]*:x:\d+:\d+:[^:]*:(?:/[A-Za-z0-9_-]*)+:(?:/[A-Za-z0-9_-]*)+\n)+})
47
+
48
+ # The test file for Windows systems.
49
+ WINDOWS_TEST_FILE = TestFile.new('\\boot.ini', /\[boot loader\](?:\r?\n(?:[^\[\r\n].*)?)*\r?\n(?:\[operating system\](?:\r?\n(?:[^\[\r\n].*)?)*\r?\n)?/m)
50
+
51
+ # The default directory traversal depth.
52
+ DEFAULT_DEPTH = 6
53
+
54
+ # Targeted Operating System (OS)
55
+ #
56
+ # @return [:unix, :windows, nil]
57
+ attr_reader :os
58
+
59
+ # Optional filter bypass technique to use.
60
+ #
61
+ # @return [:null_byte, :base64, :rot13, :zlib, nil]
62
+ attr_reader :filter_bypass
63
+
64
+ # The number of directories to traverse up
65
+ #
66
+ # @return [Integer]
67
+ attr_reader :depth
68
+
69
+ # The directory separator character.
70
+ #
71
+ # @return [String]
72
+ attr_reader :separator
73
+
74
+ # The escape path to add to every LFI path
75
+ #
76
+ # @return [String]
77
+ attr_reader :escape_path
78
+
79
+ # The common file to test with.
80
+ #
81
+ # @return [TestFile]
82
+ attr_reader :test_file
83
+
84
+ #
85
+ # Creates a new LFI object.
86
+ #
87
+ # @param [String, URI::HTTP] url
88
+ # The URL to exploit.
89
+ #
90
+ # @param [:unix, :windows, nil] os
91
+ # Operating System to specifically target.
92
+ #
93
+ # @param [Integer] depth
94
+ # Number of directories to escape up.
95
+ #
96
+ # @param [:null_byte, :double_escape, :base64, :rot13, :zlib, nil] filter_bypass
97
+ # Specifies which filter bypass technique to use.
98
+ #
99
+ # * `:null_byte - appends a `%00` null byte to the escaped path.
100
+ # **Note:* this technique only works on PHP < 5.3.
101
+ # * `:double_escape` - Double escapes the {#escape_path}
102
+ # (ex: `....//....//`).
103
+ # * `:base64` - Base64 encodes the included local file.
104
+ # * `:rot13` - ROT13 encodes the included local file.
105
+ # * `:zlib` - Zlib compresses and Base64 encodes the included local
106
+ # file.
107
+ #
108
+ # @param [Hash{Symbol => Object}] kwargs
109
+ # Additional keyword arguments for {WebVuln#initialize}.
110
+ #
111
+ def initialize(url, os: :unix,
112
+ depth: DEFAULT_DEPTH,
113
+ filter_bypass: nil,
114
+ **kwargs)
115
+ super(url,**kwargs)
116
+
117
+ @os = os
118
+
119
+ case @os
120
+ when :unix
121
+ @separator = '/'
122
+ @test_file = UNIX_TEST_FILE
123
+ when :windows
124
+ @separator = '\\'
125
+ @test_file = WINDOWS_TEST_FILE
126
+ else
127
+ raise(ArgumentError,"unknown os keyword value (#{@os.inspect}) must be either :unix or :windows")
128
+ end
129
+
130
+ case filter_bypass
131
+ when :null_byte, :double_escape, :base64, :rot13, :zlib, nil
132
+ @filter_bypass = filter_bypass
133
+ else
134
+ raise(ArgumentError,"unknown filter_bypass keyword value (#{filter_bypass.inspect}) must be :null_byte, :double_escape, :base64, :rot13, :zlib, or nil")
135
+ end
136
+
137
+ @depth = depth
138
+ @escape_path = ("..#{@separator}" * @depth)
139
+
140
+ apply_filter_bypasses
141
+ end
142
+
143
+ private
144
+
145
+ #
146
+ # Pre-applies additional filter-bypass rules to {#escape_path}.
147
+ #
148
+ def apply_filter_bypasses
149
+ if @filter_bypass == :double_escape
150
+ # HACK: String#gsub interpretes "\\" as a special character in the
151
+ # replace string, so we must use String#gsub with a block.
152
+ @escape_path.gsub!("..#{@separator}") do
153
+ "....#{@separator}#{@separator}"
154
+ end
155
+ end
156
+ end
157
+
158
+ public
159
+
160
+ #
161
+ # Escapes the given path.
162
+ #
163
+ # @param [String] path
164
+ # The given path to escape.
165
+ #
166
+ # @return [String]
167
+ # The escaped path.
168
+ #
169
+ # @note
170
+ # Relative paths and absolute Windows paths to other drives will not
171
+ # be escaped.
172
+ #
173
+ def escape(path)
174
+ if @os == :windows && path.start_with?('C:\\')
175
+ # escape absolute Windows paths to the C: drive
176
+ "#{@escape_path}#{path[3..]}"
177
+ elsif @os == :windows && path =~ /\A[A-Z]:/
178
+ # pass through absolute Windows paths to other drives
179
+ path
180
+ elsif path.start_with?(@separator)
181
+ # escape absolute paths
182
+ "#{@escape_path}#{path[1..]}"
183
+ else
184
+ # pass through relative paths
185
+ path
186
+ end
187
+ end
188
+
189
+ #
190
+ # Builds a `../../..` escaped path for the given file path.
191
+ #
192
+ # @param [String] path
193
+ # The path to escape.
194
+ #
195
+ # @return [String]
196
+ # The `../../../` escaped path.
197
+ #
198
+ # @note
199
+ # * If the given path begins with `php:`, then no `../../../` prefix
200
+ # will be added.
201
+ # * If initialized with `filter_bypass: :null_byte`, then a `\0`
202
+ # character will be appended to the path.
203
+ #
204
+ def encode_payload(path)
205
+ case @filter_bypass
206
+ when :base64
207
+ "php://filter/convert.base64-encode/resource=#{path}"
208
+ when :rot13
209
+ "php://filter/read=string.rot13/resource=#{path}"
210
+ when :zlib
211
+ "php://filter/zlib.deflate/convert.base64-encode/resource=#{path}"
212
+ when :null_byte
213
+ "#{escape(path)}\0"
214
+ else
215
+ escape(path)
216
+ end
217
+ end
218
+
219
+ #
220
+ # Determines whether the URL is vulnerable to Local File Inclusion (LFI).
221
+ #
222
+ # @return [Boolean]
223
+ #
224
+ def vulnerable?
225
+ response = exploit(@test_file.path)
226
+ body = response.body
227
+
228
+ case @filter_bypass
229
+ when :base64
230
+ body.scan(Text::Patterns::BASE64).any? do |string|
231
+ Base64.decode64(string) =~ @test_file
232
+ end
233
+ when :rot13
234
+ Crypto.rot(body,-13) =~ @test_file
235
+ when :zlib
236
+ body.scan(Text::Patterns::BASE64).any? do |string|
237
+ begin
238
+ Compression.zlib_inflate(Base64.decode64(string)) =~ @test_file
239
+ rescue Zlib::DataError
240
+ end
241
+ end
242
+ else
243
+ body =~ @test_file
244
+ end
245
+ end
246
+
247
+ #
248
+ # Returns the type or kind of vulnerability.
249
+ #
250
+ # @return [Symbol]
251
+ #
252
+ # @note
253
+ # This is used internally to map an vulnerability class to a printable
254
+ # type.
255
+ #
256
+ # @api private
257
+ #
258
+ # @abstract
259
+ #
260
+ def self.vuln_type
261
+ :lfi
262
+ end
263
+
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-vulns - A Ruby library for blind vulnerability testing.
4
+ #
5
+ # Copyright (c) 2022 Hal Brodigan (postmodern.mod3 at gmail.com)
6
+ #
7
+ # ronin-vulns is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-vulns is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-vulns. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/vulns/web_vuln'
22
+
23
+ require 'chars'
24
+ require 'cgi'
25
+
26
+ module Ronin
27
+ module Vulns
28
+ #
29
+ # Represents an Open Redirect vulnerability.
30
+ #
31
+ # ## Features
32
+ #
33
+ # * Checks 301, 302, 303, 307, and 308 HTTP redirects.
34
+ # * Checks `meta` refresh redirects.
35
+ # * Includes random alpha-numeric data in the test values.
36
+ #
37
+ class OpenRedirect < WebVuln
38
+
39
+ # The desired redirect URL to use in the test.
40
+ #
41
+ # @return [String]
42
+ attr_reader :test_url
43
+
44
+ #
45
+ # Initializes the Open Redirect vulnerability.
46
+ #
47
+ # @param [String, URI::HTTP] url
48
+ # The URL to exploit.
49
+ #
50
+ # @param [String] test_url
51
+ # The desired redirect URL to test the URL with.
52
+ #
53
+ def initialize(url, test_url: self.class.random_test_url, **kwargs)
54
+ super(url,**kwargs)
55
+
56
+ @test_url = test_url
57
+ end
58
+
59
+ #
60
+ # Generates a random redirect URL to use in tests.
61
+ #
62
+ # @return [String]
63
+ # A random URL to https://ronin-rb.dev/vulns/open_redirect.html.
64
+ #
65
+ # @api private
66
+ #
67
+ def self.random_test_url
68
+ "https://ronin-rb.dev/vulns/open_redirect.html?id=#{Chars::ALPHA_NUMERIC.random_string(5)}"
69
+ end
70
+
71
+ #
72
+ # Tests whther the URL has a vulnerable Open Redirect.
73
+ #
74
+ # @return [Boolean]
75
+ #
76
+ def vulnerable?
77
+ response = exploit(@test_url)
78
+
79
+ case response.code
80
+ when '301', '302', '303', '307', '308'
81
+ if (locations = response.get_fields('Location'))
82
+ escaped_test_url = Regexp.escape(@test_url)
83
+ regexp = %r{\A#{escaped_test_url}(?:[\?&].+)?\z}
84
+
85
+ locations.last =~ regexp
86
+ end
87
+ else
88
+ content_type = response.content_type
89
+
90
+ if content_type && content_type.include?('text/html')
91
+ escaped_test_url = Regexp.escape(CGI.escapeHTML(@test_url))
92
+ regexp = %r{<meta\s+http-equiv\s*=\s*(?:"refresh"|'refresh'|refresh)\s+content\s*=\s*(?:"\s*\d+\s*;\s*url\s*=\s*'\s*#{escaped_test_url}\s*'\s*"|'\s*\d+\s*;\s*url\s*=\s*"\s*#{escaped_test_url}\s*"\s*'|\s*\d+;url=(?:"#{escaped_test_url}"|'#{escaped_test_url}'))\s*(?:/\s*)?>}i
93
+
94
+ response.body =~ regexp
95
+ end
96
+ end
97
+ end
98
+
99
+ #
100
+ # Returns the type or kind of vulnerability.
101
+ #
102
+ # @return [Symbol]
103
+ #
104
+ # @note
105
+ # This is used internally to map an vulnerability class to a printable
106
+ # type.
107
+ #
108
+ # @api private
109
+ #
110
+ # @abstract
111
+ #
112
+ def self.vuln_type
113
+ :open_redirect
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+ # frozen_string_literal: true
3
+ #
4
+ # ronin-vulns - A Ruby library for blind vulnerability testing.
5
+ #
6
+ # Copyright (c) 2022 Hal Brodigan (postmodern.mod3 at gmail.com)
7
+ #
8
+ # ronin-vulns is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU Lesser General Public License as published
10
+ # by the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # ronin-vulns is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU Lesser General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU Lesser General Public License
19
+ # along with ronin-vulns. If not, see <https://www.gnu.org/licenses/>.
20
+ #
21
+
22
+ require 'ronin/vulns/web_vuln'
23
+
24
+ module Ronin
25
+ module Vulns
26
+ class ReflectedXSS < WebVuln
27
+ #
28
+ # Represents information about the context which the XSS occurs within.
29
+ #
30
+ class Context
31
+
32
+ # Where in the HTML the XSS occurs.
33
+ #
34
+ # @return [:double_quoted_attr_value, :single_quoted_attr_value, :unquoted_attr_value, :attr_name, :attr_list, :tag_name, :tag_body]
35
+ # The context which the XSS occurs in.
36
+ # * `:tag_body` occurred within a tag's body (ex: `<tag>XSS...</tag>`)
37
+ # * `:double_quoted_attr_value` - occurred in a double quoted
38
+ # attribute value (ex: `<tag name="XSS">...</tag>`)
39
+ # * `:single_quoted_attr_value` - occurred in a single quoted
40
+ # attribute value (ex: `<tag name='XSS'>...</tag>`)
41
+ # * `:unquoted_attr_value` - occurred in an unquoted attribute value
42
+ # (ex: `<tag name=XSS>...</tag>`)
43
+ # * `:attr_name` - occurred in an attribute name
44
+ # (ex: `<tag nameXSS ...>`)
45
+ # * `:attr_list` - occurred in the attribute list
46
+ # (ex: `<tag XSS>...</tag>`)
47
+ # * `:tag_name` - occurred in the tag name (ex: `<tagXSS>...</tag>`)
48
+ #
49
+ # @api public
50
+ attr_reader :location
51
+
52
+ # The name of the parent tag which the XSS occurs in.
53
+ #
54
+ # @return [String]
55
+ #
56
+ # @api public
57
+ attr_reader :tag
58
+
59
+ # The attribute name that the XSS occurs in.
60
+ #
61
+ # @return [String, nil]
62
+ #
63
+ # @api public
64
+ attr_reader :attr
65
+
66
+ #
67
+ # Initializes the context.
68
+ #
69
+ # @param [:double_quoted_attr_value, :single_quoted_attr_value, :unquoted_attr_value, :attr_name, :attr_list, :tag_name, :tag_body] location
70
+ #
71
+ # @param [String] tag
72
+ #
73
+ # @param [String, nil] attr
74
+ #
75
+ # @api private
76
+ #
77
+ def initialize(location, tag: nil, attr: nil)
78
+ @location = location
79
+
80
+ @tag = tag
81
+ @attr = attr
82
+ end
83
+
84
+ # HTML identifier regexp
85
+ #
86
+ # @api private
87
+ IDENTIFIER = /[A-Za-z0-9_-]+/
88
+
89
+ # HTML attribute name regexp.
90
+ #
91
+ # @api private
92
+ ATTR_NAME = IDENTIFIER
93
+
94
+ # HTML attribute regexp.
95
+ #
96
+ # @api private
97
+ ATTR = /#{ATTR_NAME}(?:\s*=\s*"[^"]+"|\s*=\s*'[^']+'|=[^"'\s]+)?/
98
+
99
+ # HTML attribute list regexp.
100
+ #
101
+ # @api private
102
+ ATTR_LIST = /(?:\s+#{ATTR})*/
103
+
104
+ # HTML tag name regexp.
105
+ #
106
+ # @api private
107
+ TAG_NAME = IDENTIFIER
108
+
109
+ # Regexp matching when an XSS occurs within a tag's inner HTML.
110
+ #
111
+ # @api private
112
+ IN_TAG_BODY = %r{<(#{TAG_NAME})#{ATTR_LIST}\s*(?:>|/>)[^<>]*\z}
113
+
114
+ # Regexp matching when an XSS occurs within a double-quoted attribute
115
+ # value.
116
+ #
117
+ # @api private
118
+ IN_DOUBLE_QUOTED_ATTR_VALUE = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})\s*=\s*"[^"]+\z}
119
+
120
+ # Regexp matching when an XSS occurs within a single-quoted attribute
121
+ # value.
122
+ #
123
+ # @api private
124
+ IN_SINGLE_QUOTED_ATTR_VALUE = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})\s*=\s*'[^']+\z}
125
+
126
+ # Regexp matching when an XSS occurs within an unquoted attribute value.
127
+ #
128
+ # @api private
129
+ IN_UNQUOTED_ATTR_VALUE = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})=[^"'\s]+\z}
130
+
131
+ # Regexp matching when an XSS occurs within an attribute's name.
132
+ #
133
+ # @api private
134
+ IN_ATTR_NAME = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+(#{ATTR_NAME})\z}
135
+
136
+ # Regexp matching when an XSS occurs within a tag's attribute list.
137
+ #
138
+ # @api private
139
+ IN_ATTR_LIST = %r{<(#{TAG_NAME})#{ATTR_LIST}\s+\z}
140
+
141
+ # Regexp matching when an XSS occurs within a tag's name.
142
+ #
143
+ # @api private
144
+ IN_TAG_NAME = %r{<(#{TAG_NAME})\z}
145
+
146
+ #
147
+ # Determine the context of the XSS by checking the characters that come
148
+ # before the given index.
149
+ #
150
+ # @param [String] body
151
+ # The HTML response body to inspect.
152
+ #
153
+ # @param [Integer] index
154
+ # The index which the XSS occurs at.
155
+ #
156
+ # @return [Context]
157
+ # The context which the XSS occurs in.
158
+ #
159
+ # @api private
160
+ #
161
+ def self.identify(body,index)
162
+ prefix = body[0,index]
163
+
164
+ if (match = prefix.match(IN_TAG_BODY))
165
+ new(:tag_body, tag: match[1])
166
+ elsif (match = prefix.match(IN_DOUBLE_QUOTED_ATTR_VALUE))
167
+ new(:double_quoted_attr_value, tag: match[1], attr: match[2])
168
+ elsif (match = prefix.match(IN_SINGLE_QUOTED_ATTR_VALUE))
169
+ new(:single_quoted_attr_value, tag: match[1], attr: match[2])
170
+ elsif (match = prefix.match(IN_UNQUOTED_ATTR_VALUE))
171
+ new(:unquoted_attr_value, tag: match[1], attr: match[2])
172
+ elsif (match = prefix.match(IN_ATTR_NAME))
173
+ new(:attr_name, tag: match[1], attr: match[2])
174
+ elsif (match = prefix.match(IN_ATTR_LIST))
175
+ new(:attr_list, tag: match[1])
176
+ elsif (match = prefix.match(IN_TAG_NAME))
177
+ new(:tag_name, tag: match[1])
178
+ end
179
+ end
180
+
181
+ # The minimum set of required characters needed for an XSS.
182
+ #
183
+ # @api private
184
+ MINIMAL_REQUIRED_CHARS = Set['>', ' ', '/', '<']
185
+
186
+ # The mapping of contexts and their required characters.
187
+ #
188
+ # @api private
189
+ REQUIRED_CHARS = {
190
+ double_quoted_attr_value: MINIMAL_REQUIRED_CHARS + ['"'],
191
+ single_quoted_attr_value: MINIMAL_REQUIRED_CHARS + ["'"],
192
+ unquoted_attr_value: MINIMAL_REQUIRED_CHARS,
193
+
194
+ attr_name: MINIMAL_REQUIRED_CHARS,
195
+ attr_list: MINIMAL_REQUIRED_CHARS,
196
+ tag_name: MINIMAL_REQUIRED_CHARS,
197
+ tag_body: MINIMAL_REQUIRED_CHARS
198
+ }
199
+
200
+ #
201
+ # Determines if the XSS is viable, given the context and the allowed
202
+ # characters.
203
+ #
204
+ # @param [Set<String>] allowed_chars
205
+ # The allowed characters.
206
+ #
207
+ # @return [Boolean]
208
+ # Specifies whether enough characters are allowed to perform an XSS in
209
+ # the given context.
210
+ #
211
+ # @api private
212
+ #
213
+ def viable?(allowed_chars)
214
+ required_chars = REQUIRED_CHARS.fetch(@location) do
215
+ raise(NotImplementedError,"cannot determine viability for unknown XSS location type: #{@location.inspect}")
216
+ end
217
+
218
+ allowed_chars.superset?(required_chars)
219
+ end
220
+
221
+ end
222
+ end
223
+ end
224
+ end