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 +5 -5
- data/.editorconfig +17 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +40 -0
- data/CHANGELOG.md +116 -1
- data/Gemfile +2 -0
- data/README.md +90 -1
- data/Rakefile +2 -0
- data/lib/rack/utf8_sanitizer.rb +122 -29
- data/rack-utf8_sanitizer.gemspec +9 -8
- data/test/test_utf8_sanitizer.rb +422 -14
- metadata +19 -14
- data/.travis.yml +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 14d5b3232eb52814e46f5960b435851d0b0a6bdd6d18cfb1220adc08c5ecdb1e
|
|
4
|
+
data.tar.gz: 40d9329c056a74dd0328ac7af2fa4183799cb451cd1795630f6754f24718324a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,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
|
-
|
|
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
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
data/lib/rack/utf8_sanitizer.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?(
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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[
|
|
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 =
|
|
111
|
-
|
|
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 =
|
|
239
|
+
decoded = $1.hex.chr
|
|
156
240
|
|
|
157
|
-
|
|
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
|
-
|
|
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
|
data/rack-utf8_sanitizer.gemspec
CHANGED
|
@@ -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.
|
|
6
|
-
gem.authors = ["
|
|
6
|
+
gem.version = '1.11.1'
|
|
7
|
+
gem.authors = ["Catherine"]
|
|
8
|
+
gem.license = "MIT"
|
|
7
9
|
gem.email = ["whitequark@whitequark.org"]
|
|
8
|
-
gem.description =
|
|
9
|
-
|
|
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 = "
|
|
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 = '>=
|
|
19
|
+
gem.required_ruby_version = '>= 2.3'
|
|
19
20
|
|
|
20
|
-
gem.add_dependency "rack", '
|
|
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"
|
data/test/test_utf8_sanitizer.rb
CHANGED
|
@@ -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 ==
|
|
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"
|
|
143
|
-
@uri_input = "http://bar/bar+baz"
|
|
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.
|
|
4
|
+
version: 1.11.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
8
|
-
autorequire:
|
|
7
|
+
- Catherine
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
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:
|
|
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: '
|
|
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
|
-
|
|
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.
|