rack-utf8_sanitizer 1.3.1 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a21608edab6e71fde026d8c9b5fdf6afaeb831ab
4
- data.tar.gz: d3648a488dd74a3ea6357d39cc87b1d486af2c1c
2
+ SHA256:
3
+ metadata.gz: 14d5b3232eb52814e46f5960b435851d0b0a6bdd6d18cfb1220adc08c5ecdb1e
4
+ data.tar.gz: 40d9329c056a74dd0328ac7af2fa4183799cb451cd1795630f6754f24718324a
5
5
  SHA512:
6
- metadata.gz: 8c8c7e485b0ca9584951b2689b090feaf4441eb96c1fb5ee3fd8f7c79bddcb4bf4d005b69e56a1fd0675b1e96de07dca21906ccca4b255e3893007278e819afe
7
- data.tar.gz: b8dcdd43af94a1277b978184ba8e5b26c782f269f646805839d102cf216f7e8d1e375a4104ace50357c23c3761afd00acaef441ac13bcc05ae1555e719e71079
6
+ metadata.gz: 11ffb7389bcbe67ef49ce1d29829de920d72c44799c623924dd0b93e578d89414de07bb4ef315d50d13e5463825b1d4727f999c10faafb1d507e52875caa09be
7
+ data.tar.gz: 4b9115c9909dc1cfd8378451f0974808f5d834bd7daaaca012d04413e48ed80cc5f8bcb10f6307300b982cf5f56080dafada5963126b5563df18ff83772dcfd8
data/.editorconfig ADDED
@@ -0,0 +1,17 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.md]
12
+ indent_style = space
13
+ indent_size = 2
14
+
15
+ [*.y{a,}ml]
16
+ indent_style = space
17
+ indent_size = 2
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+
8
+ runs-on: ${{ matrix.os }}
9
+
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest]
14
+ ruby:
15
+ - 2.5
16
+ - 2.6
17
+ - 2.7
18
+ - "3.0"
19
+ - 3.1
20
+ - 3.2
21
+ - 3.3
22
+ - 3.4
23
+ - ruby-head
24
+ - jruby-9.3
25
+ - jruby-9.4
26
+ - jruby-10.0
27
+ - jruby-head
28
+ include:
29
+ - os: ubuntu-22.04
30
+ ruby: jruby-9.2
31
+
32
+ steps:
33
+ - uses: actions/checkout@v6
34
+ - name: Set up Ruby
35
+ uses: ruby/setup-ruby@v1
36
+ with:
37
+ bundler-cache: true # 'bundle install' and cache gems
38
+ ruby-version: ${{ matrix.ruby }}
39
+ - name: Run tests
40
+ run: bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,7 +1,119 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
- Master
4
+ v1.11.0 (2025-12-04)
5
+ -------------------------
6
+
7
+ Bugs fixed:
8
+
9
+ * Return HTTP 400 when Content-Length is too large (Benjamin Quorning, #103)
10
+
11
+ v1.10.1 (2025-01-10)
12
+ -------------------------
13
+
14
+ Bugs fixed:
15
+
16
+ * Fix `URI::RFC2396_PARSER` issue with older Rubies (Tekin Süleyman, #94)
17
+
18
+
19
+ v1.10.0 (2025-01-08)
20
+ -------------------------
21
+
22
+ Changes:
23
+
24
+ * Require Ruby 2.3.0+. (Jean Boussier, #80)
25
+
26
+ Bugs fixed:
27
+
28
+ * Skip sanitizing the request body if the charset is non-utf-8 (#84)
29
+ * Don't use a mutable constant as Rack response (Jean Boussier, #86)
30
+
31
+ Chores:
32
+
33
+ * Add the `frozen_string_literal` header (Benjamin Quorning, #90)
34
+ * Avoid deprecation warming by switching from `URI::DEFAULT_PARSER` to `URI::RFC2396_PARSER` (Roman Gaufman, #92)
35
+
36
+ Performance:
37
+
38
+ * Use Content-Length to read the request body if available (Jean Boussier, #80)
39
+ * Avoid 2nd degree polynomial regexp for sanitizing content type (Jean Boussier, #82)
40
+ * Use `Regexp#match?` over `String#=~` when testing for null bytes (Geoff Harcourt, #85)
41
+
42
+ v1.9.1 (2023-08-31)
43
+ -------------------------
44
+
45
+ Bugs fixed:
46
+
47
+ * Fix null byte sanitisation (Szymon Madeja, #78)
48
+
49
+ v1.9.0 (2023-07-06)
50
+ -------------------------
51
+
52
+ * Optionally sanitize null bytes (James Coleman, #75)
53
+ * CI: add Ruby 3.2 (Peter Goldstein, #71)
54
+
55
+ v1.8.0 (2022-10-25)
56
+ -------------------------
57
+
58
+ Bugs fixed:
59
+
60
+ * Handle EOFError (Kir Shatrov, #57)
61
+
62
+ Features implemented:
63
+
64
+ * Allow Rack version 3 (Alexander Popov, #66)
65
+ * Various CI chores (Olle Jonsson)
66
+ * Move to GitHub Actions, configure Dependabot (Peter Goldstein, #62, #64)
67
+
68
+ v1.7.0 (2020-05-05)
69
+ -------------------------
70
+
71
+ * Resolve Ruby warnings about `URI.escape` (Alexander Popov, #53)
72
+ * README: better reflect that this also can sanitize text bodies (Zach McCormick, #47)
73
+ * Update documentation on exception strategy handler (Josh Frankel, #52)
74
+
75
+ v1.6.0 (2018-06-06)
76
+ -------------------------
77
+
78
+ Bugs fixed:
79
+
80
+ * Add sanitation of cookie header (John Hager, #45)
81
+
82
+ v1.5.0 (2018-02-16)
83
+ -------------------------
84
+
85
+ Bugs fixed:
86
+
87
+ * Sanitize `nil` in `sanitize_uri_encoded_string` (David Čepelík, #44)
88
+
89
+ Features implemented:
90
+
91
+ * Add `:only` and `:except` options (John Hager, #43)
92
+ * Add strategies to rack-utf8_sanitizer (John Hager, #41)
93
+
94
+ ```rb
95
+ # Example usage in Rails config/application.rb:
96
+ config.middleware.insert(0, Rack::UTF8Sanitizer, strategy: :exception)
97
+ ```
98
+
99
+ v1.4.0 (2016-03-07)
100
+ -------------------------
101
+
102
+ Performance:
103
+
104
+ * Use more performant `%char` decoding `.hex.chr` (Martin Emde, #36)
105
+ * Make `HTTP_` a constant to avoid creating the string every loop (Martin Emde, #35)
106
+
107
+ Features implemented:
108
+
109
+ * Add SERVER_NAME to list of sanitization (Denis Lysenko, 9644371)
110
+
111
+ Chores:
112
+
113
+ * Add license to gemspec (Robert Reiz, #38)
114
+
115
+
116
+ v1.3.2 (2015-12-23)
5
117
  -------------------------
6
118
 
7
119
  API modifications:
@@ -10,6 +122,9 @@ Features implemented:
10
122
 
11
123
  Bugs fixed:
12
124
 
125
+ * Strip UTF-8 Byte Order Mark from the request body (Jean Boussier, #29)
126
+ * Add options to #initialize to allow configurable sanitizable content types (Shelby Switzer, #30)
127
+
13
128
  v1.3.1 (2015-07-09)
14
129
  -------------------------
15
130
 
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in rack-utf8_sanitizer.gemspec
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Rack::UTF8Sanitizer
2
2
 
3
- Rack::UTF8Sanitizer is a Rack middleware which cleans up invalid UTF8 characters in request URI and headers.
3
+ Rack::UTF8Sanitizer is a Rack middleware which cleans up invalid UTF8 characters in request URI and headers. Additionally,
4
+ it cleans up invalid UTF8 characters in the request body (depending on the configurable content type filters) by reading
5
+ the input into a string, sanitizing the string, then replacing the Rack input stream with a rewindable input stream backed
6
+ by the sanitized string.
4
7
 
5
8
  ## Installation
6
9
 
@@ -43,6 +46,90 @@ For fields with "raw data", the algorithm is applied once and the (UTF-8 encoded
43
46
 
44
47
  For fields with "percent-encoded data", the algorithm is applied twice to catch both invalid characters appearing as-is and invalid characters appearing in the percent encoding. The percent encoded, ASCII-8BIT encoded result is left in the environment.
45
48
 
49
+ ### Sanitizable content types
50
+
51
+ The default content types to be sanitized are 'text/plain', 'application/x-www-form-urlencoded', 'application/json', 'text/javascript'. You may wish to modify this, for example if your app accepts specific or custom media types in the CONTENT_TYPE header. If you want to change the sanitizable content types, you can pass options when using Rack::UTF8Sanitizer.
52
+
53
+ To add sanitizable content types to the list of defaults, pass the `additional_content_types` options when using Rack::UTF8Sanitizer, e.g.
54
+
55
+ ``` ruby
56
+ config.middleware.insert 0, Rack::UTF8Sanitizer, additional_content_types: ['application/vnd.api+json']
57
+ ```
58
+
59
+ To explicitly set sanitizable content types and override the defaults, use the `sanitizable_content_types` option:
60
+
61
+ ``` ruby
62
+ config.middleware.insert 0, Rack::UTF8Sanitizer, sanitizable_content_types: ['application/vnd.api+json']
63
+ ```
64
+
65
+ ### Whitelist/Blacklist Rack Env Keys
66
+
67
+ Using the `:only` and `:except` keys you can skip sanitation of values in the Rack Env. `:only` and `:except` are arrays that can contain strings or regular expressions.
68
+
69
+ Only sanitize the body, query string, and url of a request.
70
+
71
+ ```ruby
72
+ config.middleware.insert 0, Rack::UTF8Sanitizer, only: ['rack.input', 'PATH_INFO', 'QUERY_STRING']
73
+ ```
74
+
75
+ Sanitize everything except HTTP headers.
76
+
77
+ ```ruby
78
+ config.middleware.insert 0, Rack::UTF8Sanitizer, except: [/HTTP_.+/]
79
+ ```
80
+
81
+ ### Strategies
82
+
83
+ There are two built in strategies for handling invalid characters. The default strategy is `:replace`, which will cause any invalid characters to be replaces with the unicode replacement character (�). The second built in strategy is `:exception` which will cause an `EncodingError` exception to be raised if invalid characters are found (the exception can then be handled by another Rack middleware).
84
+
85
+ This is an example of handling the `:exception` strategy with additional middleware:
86
+
87
+ ```ruby
88
+ require "./your/middleware/directory/utf8_sanitizer_exception_handler.rb"
89
+
90
+ config.middleware.insert 0, Rack::UTF8SanitizerExceptionHandler
91
+ config.middleware.insert_after Rack::UTF8SanitizerExceptionHandler, Rack::UTF8Sanitizer, strategy: :exception
92
+ ```
93
+
94
+ Note: The exception handling middleware must be inserted before `Rack::UTF8Sanitizer`
95
+
96
+ ```ruby
97
+ module Rack
98
+ class UTF8SanitizerExceptionHandler
99
+ def initialize(app)
100
+ @app = app
101
+ end
102
+
103
+ def call(env)
104
+ @app.call(env)
105
+ rescue EncodingError => exception
106
+ # OPTIONAL: Add error logging service of your choice here
107
+ return [400, {}, ["Bad Request"]]
108
+ end
109
+ end
110
+ end
111
+ ```
112
+
113
+ An object that responds to `#call` and accepts the offending string with invalid characters as an argument can also be passed as a `:strategy`. This is how you can define custom strategies.
114
+
115
+ ```ruby
116
+ config.middleware.insert 0, Rack::UTF8Sanitizer, strategy: :exception
117
+ ```
118
+
119
+ ```ruby
120
+ replace_string = lambda do |_invalid, sanitize_null_bytes: false|
121
+ Rails.logger.warn('Replacing invalid string')
122
+
123
+ '<Bad Encoding>'.freeze
124
+ end
125
+
126
+ config.middleware.insert 0, Rack::UTF8Sanitizer, strategy: replace_string
127
+ ```
128
+
129
+ ### Sanitizing Null Bytes
130
+
131
+ While null bytes are valid UTF-8, it can be useful to further restrict the valid character set to exclude null bytes. For example, PostgreSQL text columns do not allow storing null bytes. Passing `sanitize_null_bytes: true` in the configuration hash enables sanitizing null bytes, and the two built-in strategies both support this feature. Custom strategies should accept a keyword argument `sanitize_null_bytes` containing this configuration value.
132
+
46
133
  ## Contributing
47
134
 
48
135
  1. Fork it
@@ -50,3 +137,5 @@ For fields with "percent-encoded data", the algorithm is applied twice to catch
50
137
  3. Commit your changes (`git commit -am 'Add some feature'`)
51
138
  4. Push to the branch (`git push origin my-new-feature`)
52
139
  5. Create new Pull Request
140
+
141
+ To run the tests, run `rake spec` in the project directory.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
 
3
5
  task :default => :spec
@@ -1,21 +1,63 @@
1
1
  # encoding: ascii-8bit
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'uri'
4
5
  require 'stringio'
6
+ require 'rack/request'
5
7
 
6
8
  module Rack
7
9
  class UTF8Sanitizer
8
10
  StringIO = ::StringIO
11
+ NULL_BYTE_REGEX = /\x00/.freeze
9
12
 
10
- def initialize(app)
13
+ class InvalidStream < IOError; end
14
+ class NullByteInString < StandardError; end
15
+
16
+ # options[:sanitizable_content_types] Array
17
+ # options[:additional_content_types] Array
18
+ def initialize(app, options={})
11
19
  @app = app
20
+ @strategy = build_strategy(options)
21
+ @sanitizable_content_types = options[:sanitizable_content_types]
22
+ @sanitizable_content_types ||= SANITIZABLE_CONTENT_TYPES + (options[:additional_content_types] || [])
23
+ @only = Array(options[:only]).flatten
24
+ @except = Array(options[:except]).flatten
25
+ @sanitize_null_bytes = options.fetch(:sanitize_null_bytes, false)
12
26
  end
13
27
 
14
28
  def call(env)
15
- @app.call(sanitize(env))
29
+ begin
30
+ env = sanitize(env)
31
+ rescue EOFError, InvalidStream
32
+ return [400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
33
+ end
34
+ @app.call(env)
16
35
  end
17
36
 
18
- # http://rack.rubyforge.org/doc/SPEC.html
37
+ DEFAULT_STRATEGIES = {
38
+ replace: lambda do |input, sanitize_null_bytes: false|
39
+ input.
40
+ force_encoding(Encoding::ASCII_8BIT).
41
+ encode!(Encoding::UTF_8,
42
+ invalid: :replace,
43
+ undef: :replace)
44
+ if sanitize_null_bytes
45
+ input = input.gsub(NULL_BYTE_REGEX, "")
46
+ end
47
+ input
48
+ end,
49
+ exception: lambda do |input, sanitize_null_bytes: false|
50
+ input.
51
+ force_encoding(Encoding::ASCII_8BIT).
52
+ encode!(Encoding::UTF_8)
53
+ if sanitize_null_bytes && NULL_BYTE_REGEX.match?(input)
54
+ raise NullByteInString
55
+ end
56
+ input
57
+ end
58
+ }.freeze
59
+
60
+ # https://github.com/rack/rack/blob/main/SPEC.rdoc
19
61
  URI_FIELDS = %w(
20
62
  SCRIPT_NAME
21
63
  REQUEST_PATH REQUEST_URI PATH_INFO
@@ -23,26 +65,32 @@ module Rack
23
65
  HTTP_REFERER
24
66
  ORIGINAL_FULLPATH
25
67
  ORIGINAL_SCRIPT_NAME
26
- )
68
+ SERVER_NAME
69
+ ).freeze
27
70
 
28
71
  SANITIZABLE_CONTENT_TYPES = %w(
29
72
  text/plain
30
73
  application/x-www-form-urlencoded
31
74
  application/json
32
75
  text/javascript
33
- )
76
+ ).freeze
34
77
 
35
78
  URI_ENCODED_CONTENT_TYPES = %w(
36
79
  application/x-www-form-urlencoded
37
- )
80
+ ).freeze
81
+
82
+ HTTP_ = 'HTTP_'
38
83
 
39
84
  def sanitize(env)
40
85
  sanitize_rack_input(env)
86
+ sanitize_cookies(env)
41
87
  env.each do |key, value|
88
+ next if skip?(key)
89
+
42
90
  if URI_FIELDS.include?(key)
43
91
  env[key] = transfer_frozen(value,
44
92
  sanitize_uri_encoded_string(value))
45
- elsif key.to_s.start_with?("HTTP_")
93
+ elsif key.to_s.start_with?(HTTP_)
46
94
  # Just sanitize the headers and leave them in UTF-8. There is
47
95
  # no reason to have UTF-8 in headers, but if it's valid, let it be.
48
96
  env[key] = transfer_frozen(value,
@@ -53,18 +101,33 @@ module Rack
53
101
 
54
102
  protected
55
103
 
104
+ def skip?(rack_env_key)
105
+ return true if !@except.empty? && @except.any? { |matcher| rack_env_key[matcher] }
106
+ return true if !@only.empty? && @only.none? { |matcher| rack_env_key[matcher] }
107
+
108
+ false
109
+ end
110
+
111
+ def build_strategy(options)
112
+ strategy = options.fetch(:strategy) { :replace }
113
+
114
+ return strategy unless DEFAULT_STRATEGIES.key?(strategy)
115
+
116
+ DEFAULT_STRATEGIES[strategy]
117
+ end
118
+
56
119
  def sanitize_rack_input(env)
57
- # https://github.com/rack/rack/blob/master/lib/rack/request.rb#L42
58
- # Logic borrowed from Rack::Request#media_type,#media_type_params,#content_charset
59
- # Ignoring charset in content type.
60
- content_type = env['CONTENT_TYPE']
61
- content_type &&= content_type.split(/\s*[;,]\s*/, 2).first
62
- content_type &&= content_type.downcase
63
- return unless SANITIZABLE_CONTENT_TYPES.any? {|type| content_type == type }
120
+ request = Rack::Request.new(env)
121
+ content_type = request.media_type
122
+ return unless @sanitizable_content_types.any? {|type| content_type == type }
123
+
124
+ charset = request.content_charset
125
+ return if charset && charset.downcase != 'utf-8'
126
+
64
127
  uri_encoded = URI_ENCODED_CONTENT_TYPES.any? {|type| content_type == type}
65
128
 
66
- if env["rack.input"]
67
- sanitized_input = sanitize_io(env['rack.input'], uri_encoded)
129
+ if env['rack.input']
130
+ sanitized_input = sanitize_io(env['rack.input'], uri_encoded, env['CONTENT_LENGTH']&.to_i)
68
131
 
69
132
  env['rack.input'] = sanitized_input
70
133
  env['CONTENT_LENGTH'] &&= sanitized_input.size.to_s
@@ -106,9 +169,14 @@ module Rack
106
169
  end
107
170
  end
108
171
 
109
- def sanitize_io(io, uri_encoded = false)
110
- input = io.read
111
- sanitized_input = sanitize_string(input)
172
+ def sanitize_io(io, uri_encoded = false, content_length = nil)
173
+ input = if content_length && content_length >= 0
174
+ io.read(content_length)
175
+ else
176
+ io.read
177
+ end
178
+ raise InvalidStream if input.nil?
179
+ sanitized_input = sanitize_string(strip_byte_order_mark(input))
112
180
  if uri_encoded
113
181
  sanitized_input = sanitize_uri_encoded_string(sanitized_input).
114
182
  force_encoding(Encoding::UTF_8)
@@ -117,6 +185,20 @@ module Rack
117
185
  SanitizedRackInput.new(io, StringIO.new(sanitized_input))
118
186
  end
119
187
 
188
+ # Cookies need to be split and then sanitized as url encoded strings
189
+ # since the cookie string itself is not url encoded (separated by `;`),
190
+ # and the normal method of `sanitize_uri_encoded_string` would break
191
+ # later cookie parsing in the case that a cookie value contained an
192
+ # encoded `;`.
193
+ def sanitize_cookies(env)
194
+ return unless env['HTTP_COOKIE']
195
+
196
+ env['HTTP_COOKIE'] = env['HTTP_COOKIE']
197
+ .split(/[;,] */n)
198
+ .map { |cookie| sanitize_uri_encoded_string(cookie) }
199
+ .join('; ')
200
+ end
201
+
120
202
  # URI.encode/decode expect the input to be in ASCII-8BIT.
121
203
  # However, there could be invalid UTF-8 characters both in
122
204
  # raw and percent-encoded form.
@@ -126,6 +208,7 @@ module Rack
126
208
  #
127
209
  # The result is guaranteed to be UTF-8-safe.
128
210
  def sanitize_uri_encoded_string(input)
211
+ return input if input.nil?
129
212
  decoded_value = decode_string(input)
130
213
  reencode_string(decoded_value)
131
214
  end
@@ -143,7 +226,8 @@ module Rack
143
226
 
144
227
  # This regexp matches all 'unreserved' characters from RFC3986 (2.3),
145
228
  # plus all multibyte UTF-8 characters.
146
- UNRESERVED_OR_UTF8 = /[A-Za-z0-9\-._~\x80-\xFF]/
229
+ UNRESERVED_OR_UTF8 = /[A-Za-z0-9\-._~\x80-\xFF]/.freeze
230
+ UNRESERVED_OR_UTF8_OR_NULL = /[A-Za-z0-9\-._~\x00\x80-\xFF]/.freeze
147
231
 
148
232
  # RFC3986, 2.2 states that the characters from 'reserved' group must be
149
233
  # protected during normalization (which is what UTF8Sanitizer does).
@@ -152,9 +236,10 @@ module Rack
152
236
  # enough for our task.
153
237
  def unescape_unreserved(input)
154
238
  input.gsub(/%([a-f\d]{2})/i) do |encoded|
155
- decoded = [$1.hex].pack('C')
239
+ decoded = $1.hex.chr
156
240
 
157
- if decoded =~ UNRESERVED_OR_UTF8
241
+ decodable_regex = @sanitize_null_bytes ? UNRESERVED_OR_UTF8_OR_NULL : UNRESERVED_OR_UTF8
242
+ if decoded =~ decodable_regex
158
243
  decoded
159
244
  else
160
245
  encoded
@@ -173,21 +258,21 @@ module Rack
173
258
  # Performs the reverse function of `unescape_unreserved`. Unlike
174
259
  # the previous function, we can reuse the logic in URI#encode
175
260
  def escape_unreserved(input)
176
- URI.encode(input, UNSAFE)
261
+ if Object.const_defined?("URI::RFC2396_PARSER")
262
+ URI::RFC2396_PARSER.escape(input, UNSAFE)
263
+ else
264
+ URI::DEFAULT_PARSER.escape(input, UNSAFE)
265
+ end
177
266
  end
178
267
 
179
268
  def sanitize_string(input)
180
269
  if input.is_a? String
181
270
  input = input.dup.force_encoding(Encoding::UTF_8)
182
271
 
183
- if input.valid_encoding?
272
+ if input.valid_encoding? && !(@sanitize_null_bytes && input =~ NULL_BYTE_REGEX)
184
273
  input
185
274
  else
186
- input.
187
- force_encoding(Encoding::ASCII_8BIT).
188
- encode!(Encoding::UTF_8,
189
- invalid: :replace,
190
- undef: :replace)
275
+ @strategy.call(input, sanitize_null_bytes: @sanitize_null_bytes)
191
276
  end
192
277
  else
193
278
  input
@@ -201,5 +286,13 @@ module Rack
201
286
  to
202
287
  end
203
288
  end
289
+
290
+ UTF8_BOM = "\xef\xbb\xbf".dup.force_encoding(Encoding::BINARY).freeze
291
+ UTF8_BOM_SIZE = UTF8_BOM.bytesize
292
+
293
+ def strip_byte_order_mark(input)
294
+ return input unless input.start_with?(UTF8_BOM)
295
+ input.byteslice(UTF8_BOM_SIZE..-1)
296
+ end
204
297
  end
205
298
  end
@@ -1,23 +1,24 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  Gem::Specification.new do |gem|
4
5
  gem.name = "rack-utf8_sanitizer"
5
- gem.version = '1.3.1'
6
- gem.authors = ["Peter Zotov"]
6
+ gem.version = '1.11.1'
7
+ gem.authors = ["Catherine"]
8
+ gem.license = "MIT"
7
9
  gem.email = ["whitequark@whitequark.org"]
8
- gem.description = %{Rack::UTF8Sanitizer is a Rack middleware which cleans up } <<
9
- %{invalid UTF8 characters in request URI and headers.}
10
+ gem.description = "Rack::UTF8Sanitizer is a Rack middleware which cleans up " \
11
+ "invalid UTF8 characters in request URI and headers."
10
12
  gem.summary = gem.description
11
- gem.homepage = "http://github.com/whitequark/rack-utf8_sanitizer"
13
+ gem.homepage = "https://github.com/whitequark/rack-utf8_sanitizer"
12
14
 
13
15
  gem.files = `git ls-files`.split($/)
14
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
17
  gem.require_paths = ["lib"]
17
18
 
18
- gem.required_ruby_version = '>= 1.9'
19
+ gem.required_ruby_version = '>= 2.3'
19
20
 
20
- gem.add_dependency "rack", '~> 1.0'
21
+ gem.add_dependency "rack", '>= 1.0', '< 4.0'
21
22
 
22
23
  gem.add_development_dependency "bacon"
23
24
  gem.add_development_dependency "bacon-colored_output"
@@ -1,6 +1,8 @@
1
1
  # encoding:ascii-8bit
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bacon/colored_output'
5
+ require 'cgi'
4
6
  require 'rack/utf8_sanitizer'
5
7
 
6
8
  describe Rack::UTF8Sanitizer do
@@ -28,10 +30,21 @@ describe Rack::UTF8Sanitizer do
28
30
  end
29
31
  end
30
32
 
33
+ describe "with invalid host input" do
34
+ it "sanitizes host entity (SERVER_NAME)" do
35
+ host = "host\xD0".dup.force_encoding('UTF-8')
36
+ env = @app.({ "SERVER_NAME" => host })
37
+ result = env["SERVER_NAME"]
38
+
39
+ result.encoding.should == Encoding::US_ASCII
40
+ result.should.be.valid_encoding
41
+ end
42
+ end
43
+
31
44
  describe "with invalid UTF-8 input" do
32
45
  before do
33
- @plain_input = "foo\xe0".force_encoding('UTF-8')
34
- @uri_input = "http://bar/foo%E0".force_encoding('UTF-8')
46
+ @plain_input = "foo\xe0".dup.force_encoding('UTF-8')
47
+ @uri_input = "http://bar/foo%E0".dup.force_encoding('UTF-8')
35
48
  end
36
49
 
37
50
  behaves_like :does_sanitize_plain
@@ -40,7 +53,7 @@ describe Rack::UTF8Sanitizer do
40
53
 
41
54
  describe "with invalid, incorrectly percent-encoded UTF-8 URI input" do
42
55
  before do
43
- @uri_input = "http://bar/foo%E0\xe0".force_encoding('UTF-8')
56
+ @uri_input = "http://bar/foo%E0\xe0".dup.force_encoding('UTF-8')
44
57
  end
45
58
 
46
59
  behaves_like :does_sanitize_uri
@@ -88,8 +101,8 @@ describe Rack::UTF8Sanitizer do
88
101
 
89
102
  describe "with valid UTF-8 input" do
90
103
  before do
91
- @plain_input = "foo bar лол".force_encoding('UTF-8')
92
- @uri_input = "http://bar/foo+bar+%D0%BB%D0%BE%D0%BB".force_encoding('UTF-8')
104
+ @plain_input = "foo bar лол".dup.force_encoding('UTF-8')
105
+ @uri_input = "http://bar/foo+bar+%D0%BB%D0%BE%D0%BB".dup.force_encoding('UTF-8')
93
106
  end
94
107
 
95
108
  behaves_like :identity_plain
@@ -97,7 +110,7 @@ describe Rack::UTF8Sanitizer do
97
110
 
98
111
  describe "with URI characters from reserved range" do
99
112
  before do
100
- @uri_input = "http://bar/foo+%2F%3A+bar+%D0%BB%D0%BE%D0%BB".force_encoding('UTF-8')
113
+ @uri_input = "http://bar/foo+%2F%3A+bar+%D0%BB%D0%BE%D0%BB".dup.force_encoding('UTF-8')
101
114
  end
102
115
 
103
116
  behaves_like :identity_uri
@@ -106,7 +119,8 @@ describe Rack::UTF8Sanitizer do
106
119
 
107
120
  describe "with valid, not percent-encoded UTF-8 URI input" do
108
121
  before do
109
- @uri_input = "http://bar/foo+bar+лол".force_encoding('UTF-8')
122
+ @uri_input = "http://bar/foo+bar+лол".dup.force_encoding('UTF-8')
123
+ @encoded = "http://bar/foo+bar+#{CGI.escape("лол")}"
110
124
  end
111
125
 
112
126
  it "does not change URI-like entity (REQUEST_PATH)" do
@@ -115,7 +129,7 @@ describe Rack::UTF8Sanitizer do
115
129
 
116
130
  result.encoding.should == Encoding::US_ASCII
117
131
  result.should.be.valid_encoding
118
- result.should == URI.encode(@uri_input)
132
+ result.should == @encoded
119
133
  end
120
134
  end
121
135
 
@@ -139,8 +153,8 @@ describe Rack::UTF8Sanitizer do
139
153
 
140
154
  describe "with frozen strings" do
141
155
  before do
142
- @plain_input = "bar baz".freeze
143
- @uri_input = "http://bar/bar+baz".freeze
156
+ @plain_input = "bar baz"
157
+ @uri_input = "http://bar/bar+baz"
144
158
  end
145
159
 
146
160
  it "preserves the frozen? status of input" do
@@ -152,9 +166,24 @@ describe Rack::UTF8Sanitizer do
152
166
  end
153
167
  end
154
168
 
169
+ describe "with mutable strings" do
170
+ before do
171
+ @plain_input = "bar baz".dup
172
+ @uri_input = "http://bar/bar+baz".dup
173
+ end
174
+
175
+ it "preserves the frozen? status of input" do
176
+ env = @app.({ "HTTP_USER_AGENT" => @plain_input,
177
+ "REQUEST_PATH" => @uri_input })
178
+
179
+ env["HTTP_USER_AGENT"].should.not.be.frozen
180
+ env["REQUEST_PATH"].should.not.be.frozen
181
+ end
182
+ end
183
+
155
184
  describe "with symbols in the env" do
156
185
  before do
157
- @uri_input = "http://bar/foo%E0\xe0".force_encoding('UTF-8')
186
+ @uri_input = "http://bar/foo%E0\xe0".dup.force_encoding('UTF-8')
158
187
  end
159
188
 
160
189
  it "sanitizes REQUEST_PATH with invalid UTF-8 URI input" do
@@ -170,7 +199,7 @@ describe Rack::UTF8Sanitizer do
170
199
 
171
200
  describe "with form data" do
172
201
  def request_env
173
- @plain_input = "foo bar лол".force_encoding('UTF-8')
202
+ @plain_input = "foo bar лол".dup.force_encoding('UTF-8')
174
203
  {
175
204
  "REQUEST_METHOD" => "POST",
176
205
  "CONTENT_TYPE" => "application/x-www-form-urlencoded;foo=bar",
@@ -179,8 +208,8 @@ describe Rack::UTF8Sanitizer do
179
208
  }
180
209
  end
181
210
 
182
- def sanitize_form_data(request_env = request_env)
183
- @uri_input = "http://bar/foo+%2F%3A+bar+%D0%BB%D0%BE%D0%BB".force_encoding('UTF-8')
211
+ def sanitize_form_data(request_env = request_env())
212
+ @uri_input = "http://bar/foo+%2F%3A+bar+%D0%BB%D0%BE%D0%BB".dup.force_encoding('UTF-8')
184
213
  @response_env = @app.(request_env)
185
214
  sanitized_input = @response_env['rack.input'].read
186
215
 
@@ -194,6 +223,34 @@ describe Rack::UTF8Sanitizer do
194
223
  @response_env['rack.input'].close
195
224
  end
196
225
 
226
+ class BrokenIO < StringIO
227
+ def read
228
+ raise EOFError
229
+ end
230
+ end
231
+
232
+ it "returns HTTP 400 on EOF" do
233
+ @rack_input = BrokenIO.new
234
+ @response_env = @app.(request_env)
235
+ @response_env.should == [400, {"Content-Type"=>"text/plain"}, ["Bad Request"]]
236
+ end
237
+
238
+ it "Bad Request response can safety be mutated" do
239
+ @rack_input = BrokenIO.new
240
+ response_env = @app.(request_env)
241
+ response_env.should == [400, {"Content-Type"=>"text/plain"}, ["Bad Request"]]
242
+ response_env[1]["Set-Cookie"] = "you_are_admin"
243
+
244
+ response_env = @app.(request_env)
245
+ response_env[1]["Set-Cookie"].should == nil
246
+ end
247
+
248
+ it "returns HTTP 400 if CONTENT_LENGTH is larger than actual length of rack.input" do
249
+ @rack_input = StringIO.new("")
250
+ response_env = @app.(request_env.merge("CONTENT_LENGTH" => (@rack_input.length + 1).to_s))
251
+ response_env.should == [400, {"Content-Type"=>"text/plain"}, ["Bad Request"]]
252
+ end
253
+
197
254
  it "sanitizes StringIO rack.input" do
198
255
  input = "foo=bla&quux=bar"
199
256
  @rack_input = StringIO.new input
@@ -227,6 +284,29 @@ describe Rack::UTF8Sanitizer do
227
284
  end
228
285
  end
229
286
 
287
+ it "sanitizes the rack body if the charset is present and utf-8" do
288
+ input = "name=#{CGI.escape("まつもと")}"
289
+ @rack_input = StringIO.new input
290
+
291
+ env = request_env.update('CONTENT_TYPE' => "application/x-www-form-urlencoded; charset=utf-8")
292
+ sanitize_form_data(env) do |sanitized_input|
293
+ sanitized_input.encoding.should == Encoding::UTF_8
294
+ sanitized_input.should.be.valid_encoding
295
+ sanitized_input.should == input
296
+ end
297
+ end
298
+
299
+ it "strip UTF-8 BOM from StringIO rack.input" do
300
+ input = %(\xef\xbb\xbf{"Hello": "World"})
301
+ @rack_input = StringIO.new input
302
+
303
+ sanitize_form_data(request_env.merge("CONTENT_TYPE" => "application/json")) do |sanitized_input|
304
+ sanitized_input.encoding.should == Encoding::UTF_8
305
+ sanitized_input.should.be.valid_encoding
306
+ sanitized_input.should == '{"Hello": "World"}'
307
+ end
308
+ end
309
+
230
310
  it "sanitizes StringIO rack.input with form encoded bad encoding" do
231
311
  input = "foo=bla&foo=baz&quux%ED=bar%ED"
232
312
  @rack_input = StringIO.new input
@@ -291,6 +371,18 @@ describe Rack::UTF8Sanitizer do
291
371
  end
292
372
  end
293
373
 
374
+ it "does not sanitize the rack body if the charset is present and not utf-8" do
375
+ input = "name=".encode("Shift_JIS") + CGI.escape("まつもと".encode("Shift_JIS", "UTF-8"))
376
+ @rack_input = StringIO.new input
377
+
378
+ env = request_env.update('CONTENT_TYPE' => "application/x-www-form-urlencoded; charset=Shift_JIS")
379
+ sanitize_form_data(env) do |sanitized_input|
380
+ sanitized_input.encoding.should == Encoding::SHIFT_JIS
381
+ sanitized_input.should.be.valid_encoding
382
+ sanitized_input.should == input
383
+ end
384
+ end
385
+
294
386
  it "adjusts content-length when replacing input" do
295
387
  input = "foo=bla&quux=bar\xED"
296
388
  @rack_input = StringIO.new input
@@ -301,5 +393,321 @@ describe Rack::UTF8Sanitizer do
301
393
  @response_env["CONTENT_LENGTH"].should == sanitized_input.bytesize.to_s
302
394
  end
303
395
  end
396
+
397
+ it "does not sanitize null bytes by default" do
398
+ input = "foo=bla&quux=bar%00"
399
+ @rack_input = StringIO.new input
400
+
401
+ sanitize_form_data do |sanitized_input|
402
+ sanitized_input.encoding.should == Encoding::UTF_8
403
+ sanitized_input.should.be.valid_encoding
404
+ sanitized_input.should == input
405
+ end
406
+ end
407
+
408
+ it "optionally sanitizes null bytes with the replace strategy" do
409
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, sanitize_null_bytes: true)
410
+ input = "foo=bla\xED&quux=bar\x00"
411
+ @rack_input = StringIO.new input
412
+
413
+ sanitize_form_data do |sanitized_input|
414
+ sanitized_input.encoding.should == Encoding::UTF_8
415
+ sanitized_input.should.be.valid_encoding
416
+ sanitized_input.should == "foo=bla%EF%BF%BD&quux=bar"
417
+ end
418
+ end
419
+
420
+ it "optionally sanitizes encoded null bytes with the replace strategy" do
421
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, sanitize_null_bytes: true)
422
+ input = "foo=bla%ED&quux=bar%00"
423
+ @rack_input = StringIO.new input
424
+
425
+ sanitize_form_data do |sanitized_input|
426
+ sanitized_input.encoding.should == Encoding::UTF_8
427
+ sanitized_input.should.be.valid_encoding
428
+ sanitized_input.should == "foo=bla%EF%BF%BD&quux=bar"
429
+ end
430
+ end
431
+
432
+ it "optionally raises on null bytes with the exception strategy" do
433
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, sanitize_null_bytes: true, strategy: :exception)
434
+ input = "foo=bla&quux=bar\x00"
435
+ @rack_input = StringIO.new input
436
+
437
+ should.raise(Rack::UTF8Sanitizer::NullByteInString) do
438
+ sanitize_form_data
439
+ end
440
+ end
441
+
442
+ it "optionally raises on encoded null bytes with the exception strategy" do
443
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, sanitize_null_bytes: true, strategy: :exception)
444
+ input = "foo=bla&quux=bar%00"
445
+ @rack_input = StringIO.new input
446
+
447
+ should.raise(Rack::UTF8Sanitizer::NullByteInString) do
448
+ sanitize_form_data
449
+ end
450
+ end
451
+
452
+ it "gives precedence to encoding errors with the exception strategy and null byte sanitisation" do
453
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, sanitize_null_bytes: true, strategy: :exception)
454
+ input = "foo=bla\x00&quux=bar\xED"
455
+ @rack_input = StringIO.new input
456
+
457
+ should.raise(EncodingError) do
458
+ sanitize_form_data
459
+ end
460
+ end
461
+ end
462
+
463
+ describe "with custom content-type" do
464
+ def request_env
465
+ {
466
+ "REQUEST_METHOD" => "GET",
467
+ "CONTENT_TYPE" => "application/json",
468
+ "HTTP_COOKIE" => @cookie,
469
+ "rack.input" => StringIO.new,
470
+ }
471
+ end
472
+
473
+ it "sanitizes bad http cookie" do
474
+ @cookie = "foo=bla; quux=bar\xED"
475
+ response_env = @app.(request_env)
476
+ response_env['HTTP_COOKIE'].should != @cookie
477
+ response_env['HTTP_COOKIE'].should == 'foo=bla; quux=bar%EF%BF%BD'
478
+ end
479
+
480
+ it "does not change ok http cookie" do
481
+ @cookie = "foo=bla; quux=bar"
482
+ response_env = @app.(request_env)
483
+ response_env['HTTP_COOKIE'].should == @cookie
484
+
485
+ @cookie = "foo=b%3bla; quux=b%20a%20r"
486
+ response_env = @app.(request_env)
487
+ response_env['HTTP_COOKIE'].should == @cookie
488
+ end
489
+ end
490
+
491
+ describe "with custom content-type" do
492
+ def request_env
493
+ @plain_input = "foo bar лол".dup.force_encoding('UTF-8')
494
+ {
495
+ "REQUEST_METHOD" => "POST",
496
+ "CONTENT_TYPE" => "application/vnd.api+json",
497
+ "HTTP_USER_AGENT" => @plain_input,
498
+ "rack.input" => @rack_input,
499
+ }
500
+ end
501
+
502
+ def sanitize_data(request_env = request_env())
503
+ @uri_input = "http://bar/foo+%2F%3A+bar+%D0%BB%D0%BE%D0%BB".dup.force_encoding('UTF-8')
504
+ @response_env = @app.(request_env)
505
+ sanitized_input = @response_env['rack.input'].read
506
+
507
+ yield sanitized_input if block_given?
508
+ end
509
+
510
+ it "does not sanitize custom content-type by default" do
511
+ input = "foo=bla&quux=bar\xED"
512
+ @rack_input = StringIO.new input
513
+
514
+ env = request_env
515
+ sanitize_data(env) do |sanitized_input|
516
+ sanitized_input.encoding.should == Encoding::ASCII_8BIT
517
+ sanitized_input.should.be.valid_encoding
518
+ sanitized_input.should == input
519
+ end
520
+ end
521
+
522
+ it "sanitizes custom content-type if additional_content_types given" do
523
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, additional_content_types: ["application/vnd.api+json"])
524
+ input = "foo=bla&quux=bar\xED"
525
+ @rack_input = StringIO.new input
526
+
527
+ env = request_env
528
+ sanitize_data(env) do |sanitized_input|
529
+ sanitized_input.encoding.should == Encoding::UTF_8
530
+ sanitized_input.should.be.valid_encoding
531
+ sanitized_input.should != input
532
+ end
533
+ end
534
+
535
+ it "sanitizes default content-type if additional_content_types given" do
536
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, additional_content_types: ["application/vnd.api+json"])
537
+ input = "foo=bla&quux=bar\xED"
538
+ @rack_input = StringIO.new input
539
+
540
+ env = request_env.update('CONTENT_TYPE' => 'application/json')
541
+ sanitize_data(env) do |sanitized_input|
542
+ sanitized_input.encoding.should == Encoding::UTF_8
543
+ sanitized_input.should.be.valid_encoding
544
+ sanitized_input.should != input
545
+ end
546
+ end
547
+
548
+ it "sanitizes custom content-type if sanitizable_content_types given" do
549
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, sanitizable_content_types: ["application/vnd.api+json"])
550
+ input = "foo=bla&quux=bar\xED"
551
+ @rack_input = StringIO.new input
552
+
553
+ env = request_env
554
+ sanitize_data(env) do |sanitized_input|
555
+ sanitized_input.encoding.should == Encoding::UTF_8
556
+ sanitized_input.should.be.valid_encoding
557
+ sanitized_input.should != input
558
+ end
559
+ end
560
+
561
+ it "does not sanitize default content-type if sanitizable_content_types does not include it" do
562
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, sanitizable_content_types: ["application/vnd.api+json"])
563
+ input = "foo=bla&quux=bar\xED"
564
+ @rack_input = StringIO.new input
565
+
566
+ env = request_env.update('CONTENT_TYPE' => 'application/json')
567
+ sanitize_data(env) do |sanitized_input|
568
+ sanitized_input.encoding.should == Encoding::ASCII_8BIT
569
+ sanitized_input.should.be.valid_encoding
570
+ sanitized_input.should == input
571
+ end
572
+ end
573
+ end
574
+
575
+ describe "with only and/or except options" do
576
+ before do
577
+ @plain_input = "foo\xe0".dup.force_encoding('UTF-8')
578
+ end
579
+
580
+ def request_env
581
+ {
582
+ "REQUEST_METHOD" => "POST",
583
+ "CONTENT_TYPE" => "application/json",
584
+ "HTTP_USER_AGENT" => @plain_input,
585
+ "HTTP_CUSTOM_HEADER" => @plain_input,
586
+ "rack.input" => @rack_input,
587
+ }
588
+ end
589
+
590
+ def sanitize_data(request_env = request_env())
591
+ @response_env = @app.(request_env)
592
+ end
593
+
594
+ it 'skips unless in only' do
595
+ @app = Rack::UTF8Sanitizer.new(
596
+ -> env { env },
597
+ only: ['HTTP_CUSTOM_HEADER']
598
+ )
599
+ @rack_input = StringIO.new('{}')
600
+
601
+ sanitize_data
602
+ @response_env['HTTP_CUSTOM_HEADER'].should != @plain_input
603
+ @response_env['HTTP_USER_AGENT'].should == @plain_input
604
+ end
605
+
606
+ it 'skips if in except' do
607
+ @app = Rack::UTF8Sanitizer.new(
608
+ -> env { env },
609
+ except: ['HTTP_CUSTOM_HEADER']
610
+ )
611
+ @rack_input = StringIO.new('{}')
612
+
613
+ sanitize_data
614
+ @response_env['HTTP_CUSTOM_HEADER'].should == @plain_input
615
+ @response_env['HTTP_USER_AGENT'].should != @plain_input
616
+ end
617
+
618
+ it 'works with regular expressions' do
619
+ @app = Rack::UTF8Sanitizer.new(
620
+ -> env { env },
621
+ only: ['HTTP_CUSTOM_HEADER', /(agent|input)/i]
622
+ )
623
+ @rack_input = StringIO.new(@plain_input.force_encoding(Encoding::ASCII_8BIT))
624
+
625
+ sanitize_data
626
+ @response_env['HTTP_CUSTOM_HEADER'].should != @plain_input
627
+ @response_env['HTTP_USER_AGENT'].should != @plain_input
628
+ @response_env['rack.input'].read.should != @plain_input
629
+ end
630
+ end
631
+
632
+ describe "with custom strategy" do
633
+ def request_env
634
+ @plain_input = "foo bar лол".dup.force_encoding('UTF-8')
635
+ {
636
+ "REQUEST_METHOD" => "POST",
637
+ "CONTENT_TYPE" => "application/json",
638
+ "HTTP_USER_AGENT" => @plain_input,
639
+ "rack.input" => @rack_input,
640
+ }
641
+ end
642
+
643
+ def sanitize_data(request_env = request_env())
644
+ @uri_input = "http://bar/foo+%2F%3A+bar+%D0%BB%D0%BE%D0%BB".dup.force_encoding('UTF-8')
645
+ @response_env = @app.(request_env)
646
+ sanitized_input = @response_env['rack.input'].read
647
+
648
+ yield sanitized_input if block_given?
649
+ end
650
+
651
+ it "calls a default strategy (replace)" do
652
+ @app = Rack::UTF8Sanitizer.new(-> env { env })
653
+
654
+ input = "foo=bla&quux=bar\xED"
655
+ @rack_input = StringIO.new input
656
+
657
+ env = request_env
658
+ sanitize_data(env) do |sanitized_input|
659
+ sanitized_input.encoding.should == Encoding::UTF_8
660
+ sanitized_input.should.be.valid_encoding
661
+ sanitized_input.should != input
662
+ end
663
+ end
664
+
665
+ it "calls the exception strategy" do
666
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, strategy: :exception)
667
+
668
+ input = "foo=bla&quux=bar\xED"
669
+ @rack_input = StringIO.new input
670
+
671
+ env = request_env
672
+ should.raise(EncodingError) { sanitize_data(env) }
673
+ end
674
+
675
+ it "accepts a proc as a strategy" do
676
+ truncate = -> (input, sanitize_null_bytes:) do
677
+ sanitize_null_bytes.should == false
678
+ "replace".dup.force_encoding(Encoding::UTF_8)
679
+ end
680
+
681
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, strategy: truncate)
682
+
683
+ input = "foo=bla&quux=bar\xED"
684
+ @rack_input = StringIO.new input
685
+
686
+ env = request_env
687
+ sanitize_data(env) do |sanitized_input|
688
+ sanitized_input.encoding.should == Encoding::UTF_8
689
+ sanitized_input.should.be.valid_encoding
690
+ sanitized_input.should == 'replace'
691
+ end
692
+ end
693
+
694
+ it "accepts a proc as a strategy and passes along sanitize_null_bytes" do
695
+ truncate = -> (input, sanitize_null_bytes:) do
696
+ sanitize_null_bytes.should == true
697
+ "replace".dup.force_encoding(Encoding::UTF_8)
698
+ end
699
+
700
+ @app = Rack::UTF8Sanitizer.new(-> env { env }, sanitize_null_bytes: true, strategy: truncate)
701
+ input = "foo=bla&quux=bar\x00"
702
+
703
+ @rack_input = StringIO.new input
704
+
705
+ env = request_env
706
+ sanitize_data(env) do |sanitized_input|
707
+ sanitized_input.encoding.should == Encoding::UTF_8
708
+ sanitized_input.should.be.valid_encoding
709
+ sanitized_input.should == 'replace'
710
+ end
711
+ end
304
712
  end
305
713
  end
metadata CHANGED
@@ -1,29 +1,34 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-utf8_sanitizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.11.1
5
5
  platform: ruby
6
6
  authors:
7
- - Peter Zotov
8
- autorequire:
7
+ - Catherine
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2015-07-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rack
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
18
  version: '1.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4.0'
20
22
  type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
23
25
  requirements:
24
- - - "~>"
26
+ - - ">="
25
27
  - !ruby/object:Gem::Version
26
28
  version: '1.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4.0'
27
32
  - !ruby/object:Gem::Dependency
28
33
  name: bacon
29
34
  requirement: !ruby/object:Gem::Requirement
@@ -74,8 +79,10 @@ executables: []
74
79
  extensions: []
75
80
  extra_rdoc_files: []
76
81
  files:
82
+ - ".editorconfig"
83
+ - ".github/dependabot.yml"
84
+ - ".github/workflows/ci.yml"
77
85
  - ".gitignore"
78
- - ".travis.yml"
79
86
  - CHANGELOG.md
80
87
  - Gemfile
81
88
  - LICENSE.txt
@@ -84,10 +91,10 @@ files:
84
91
  - lib/rack/utf8_sanitizer.rb
85
92
  - rack-utf8_sanitizer.gemspec
86
93
  - test/test_utf8_sanitizer.rb
87
- homepage: http://github.com/whitequark/rack-utf8_sanitizer
88
- licenses: []
94
+ homepage: https://github.com/whitequark/rack-utf8_sanitizer
95
+ licenses:
96
+ - MIT
89
97
  metadata: {}
90
- post_install_message:
91
98
  rdoc_options: []
92
99
  require_paths:
93
100
  - lib
@@ -95,16 +102,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
95
102
  requirements:
96
103
  - - ">="
97
104
  - !ruby/object:Gem::Version
98
- version: '1.9'
105
+ version: '2.3'
99
106
  required_rubygems_version: !ruby/object:Gem::Requirement
100
107
  requirements:
101
108
  - - ">="
102
109
  - !ruby/object:Gem::Version
103
110
  version: '0'
104
111
  requirements: []
105
- rubyforge_project:
106
- rubygems_version: 2.4.6
107
- signing_key:
112
+ rubygems_version: 3.6.7
108
113
  specification_version: 4
109
114
  summary: Rack::UTF8Sanitizer is a Rack middleware which cleans up invalid UTF8 characters
110
115
  in request URI and headers.
data/.travis.yml DELETED
@@ -1,13 +0,0 @@
1
- language: ruby
2
-
3
- rvm:
4
- - 1.9.2
5
- - 1.9.3
6
- - 2.0.0
7
- # 2.1, not 2.1.0 until fixed https://github.com/travis-ci/travis-ci/issues/2220
8
- - 2.1
9
- - jruby
10
- - rbx-2
11
-
12
- script:
13
- - rake spec