rack-sanitizer 2.0.4 → 2.1.0
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 +4 -4
- data/.github/workflows/ci.yml +11 -7
- data/.github/workflows/release.yml +24 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +38 -7
- data/LICENSE.txt +2 -1
- data/README.md +1 -1
- data/Rakefile +3 -1
- data/bin/rubocop +27 -0
- data/dev.yml +17 -0
- data/gemfiles/rack_2.2.gemfile +9 -0
- data/gemfiles/rack_3.0.gemfile +9 -0
- data/gemfiles/rack_3.1.gemfile +9 -0
- data/gemfiles/rack_3.2.gemfile +9 -0
- data/lib/rack/sanitizer.rb +29 -26
- data/rack-sanitizer.gemspec +5 -10
- data/test/test_sanitizer.rb +133 -52
- metadata +14 -48
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fca030fbcd503f8e7a4f61ebd1286552baf24d3b1a8607af3c02ca12dd9533e0
|
|
4
|
+
data.tar.gz: 44770acabfcea4af68db609520fd158986c72e5a033151352384a08f16174cda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a88c1f9a8f7aa00cf4b7a6c2ad9306f53218d467177bc154efcb12a9cebf7c2bb79a7404dbd0c1a813929b426a5169d714149488e92318ce4b0b0c80067971e
|
|
7
|
+
data.tar.gz: 0fc65bc666ac425913aa02abb117545e29dc193961e41a91cf4f704be14567f2271e8e028527a3e0872c2d129029500880959a3058d36eedd3150b079fc4d781
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -10,18 +10,22 @@ jobs:
|
|
|
10
10
|
strategy:
|
|
11
11
|
fail-fast: false
|
|
12
12
|
matrix:
|
|
13
|
-
ruby: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", ruby-head, jruby-9.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
ruby: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", ruby-head, jruby-9.3, jruby-head]
|
|
14
|
+
rack: ["2.2", "3.0", "3.1", "3.2"]
|
|
15
|
+
include:
|
|
16
|
+
- ruby: "3.4"
|
|
17
|
+
rubyopt: "--enable-frozen-string-literal --debug-frozen-string-literal"
|
|
18
|
+
|
|
19
|
+
env:
|
|
20
|
+
BUNDLE_GEMFILE: gemfiles/rack_${{ matrix.rack }}.gemfile
|
|
17
21
|
|
|
18
22
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
23
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
24
|
- name: Set up Ruby
|
|
21
|
-
uses: ruby/setup-ruby@v1
|
|
25
|
+
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
|
22
26
|
with:
|
|
23
27
|
bundler-cache: true # 'bundle install' and cache gems
|
|
24
28
|
ruby-version: ${{ matrix.ruby }}
|
|
25
29
|
bundler: latest
|
|
26
|
-
- name: Run tests ${{ matrix.rubyopt }}
|
|
30
|
+
- name: Run tests on Rack ${{ matrix.rack }} ${{ matrix.rubyopt }}
|
|
27
31
|
run: bundle exec rake RUBYOPT="${{ matrix.rubyopt }}"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
release:
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
environment: release
|
|
14
|
+
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
19
|
+
- name: Set up Ruby
|
|
20
|
+
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
|
21
|
+
with:
|
|
22
|
+
bundler-cache: true
|
|
23
|
+
ruby-version: 3.4.4
|
|
24
|
+
- uses: rubygems/release-gem@7f9650160c1a4e7989fdc9855807bdbd421d8b6b # v1.4.1
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,26 +1,57 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
rack-sanitizer (2.0
|
|
4
|
+
rack-sanitizer (2.1.0)
|
|
5
5
|
rack (>= 1.0, < 4.0)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
ast (2.4.3)
|
|
11
|
+
json (2.21.2)
|
|
12
|
+
language_server-protocol (3.17.0.6)
|
|
13
|
+
lint_roller (1.1.0)
|
|
14
|
+
ostruct (0.6.1)
|
|
15
|
+
parallel (2.1.0)
|
|
16
|
+
parser (3.3.12.0)
|
|
17
|
+
ast (~> 2.4.1)
|
|
18
|
+
racc
|
|
19
|
+
prism (1.9.0)
|
|
20
|
+
racc (1.8.1)
|
|
21
|
+
rack (3.1.21)
|
|
22
|
+
rainbow (3.1.1)
|
|
14
23
|
rake (13.1.0)
|
|
24
|
+
regexp_parser (2.12.0)
|
|
25
|
+
rubocop (1.90.0)
|
|
26
|
+
json (>= 2.3)
|
|
27
|
+
language_server-protocol (~> 3.17.0.2)
|
|
28
|
+
lint_roller (~> 1.1.0)
|
|
29
|
+
parallel (>= 1.10)
|
|
30
|
+
parser (>= 3.3.0.2)
|
|
31
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
32
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
33
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
34
|
+
ruby-progressbar (~> 1.7)
|
|
35
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
36
|
+
rubocop-ast (1.50.0)
|
|
37
|
+
parser (>= 3.3.7.2)
|
|
38
|
+
prism (~> 1.7)
|
|
39
|
+
rubocop-shopify (3.0.2)
|
|
40
|
+
lint_roller
|
|
41
|
+
rubocop (~> 1.72, >= 1.72.1)
|
|
42
|
+
ruby-progressbar (1.13.0)
|
|
43
|
+
unicode-display_width (3.2.0)
|
|
44
|
+
unicode-emoji (~> 4.1)
|
|
45
|
+
unicode-emoji (4.2.0)
|
|
15
46
|
|
|
16
47
|
PLATFORMS
|
|
17
48
|
ruby
|
|
18
49
|
|
|
19
50
|
DEPENDENCIES
|
|
20
|
-
|
|
21
|
-
bacon-colored_output
|
|
51
|
+
ostruct
|
|
22
52
|
rack-sanitizer!
|
|
23
53
|
rake
|
|
54
|
+
rubocop-shopify
|
|
24
55
|
|
|
25
56
|
BUNDLED WITH
|
|
26
57
|
2.5.7
|
data/LICENSE.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
Copyright (c) 2026 Shopify Inc.
|
|
1
2
|
Copyright (c) 2013 Peter Zotov
|
|
2
3
|
|
|
3
4
|
MIT License
|
|
@@ -19,4 +20,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
|
19
20
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
21
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
22
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -44,7 +44,7 @@ The generic sanitization algorithm is as follows:
|
|
|
44
44
|
1. Force the encoding to ASCII8-BIT.
|
|
45
45
|
2. Re-encode it as UTF-8, replacing invalid and undefined characters as U+FFFD.
|
|
46
46
|
|
|
47
|
-
For fields with "raw data", the algorithm is applied once
|
|
47
|
+
For fields with "raw data", the algorithm is applied once. ASCII-only results remain UTF-8. Results with non-ASCII characters use ASCII-8BIT encoding to satisfy the Rack environment specification.
|
|
48
48
|
|
|
49
49
|
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.
|
|
50
50
|
|
data/Rakefile
CHANGED
data/bin/rubocop
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# This file was generated by Bundler.
|
|
6
|
+
#
|
|
7
|
+
# The application 'rubocop' is installed as part of a gem, and
|
|
8
|
+
# this file is here to facilitate running it.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
|
12
|
+
|
|
13
|
+
bundle_binstub = File.expand_path("bundle", __dir__)
|
|
14
|
+
|
|
15
|
+
if File.file?(bundle_binstub)
|
|
16
|
+
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
|
|
17
|
+
load(bundle_binstub)
|
|
18
|
+
else
|
|
19
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
|
20
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
require "rubygems"
|
|
25
|
+
require "bundler/setup"
|
|
26
|
+
|
|
27
|
+
load Gem.bin_path("rubocop", "rubocop")
|
data/dev.yml
ADDED
data/lib/rack/sanitizer.rb
CHANGED
|
@@ -7,9 +7,11 @@ module Rack
|
|
|
7
7
|
class Sanitizer
|
|
8
8
|
# options[:sanitizable_content_types] Array
|
|
9
9
|
# options[:additional_content_types] Array
|
|
10
|
-
def initialize(app, options={})
|
|
10
|
+
def initialize(app, options = {})
|
|
11
11
|
@app = app
|
|
12
|
-
|
|
12
|
+
strategy = options.fetch(:strategy) { :replace }
|
|
13
|
+
@strategy = build_strategy(strategy)
|
|
14
|
+
@exception_strategy = strategy == :exception
|
|
13
15
|
@sanitizable_content_types = options[:sanitizable_content_types]
|
|
14
16
|
@sanitizable_content_types ||= SANITIZABLE_CONTENT_TYPES + (options[:additional_content_types] || [])
|
|
15
17
|
end
|
|
@@ -17,25 +19,28 @@ module Rack
|
|
|
17
19
|
def call(env)
|
|
18
20
|
env = sanitize(env)
|
|
19
21
|
begin
|
|
22
|
+
if @exception_strategy && env['rack.input'].is_a?(SanitizedRackInput)
|
|
23
|
+
env['rack.input'].ensure_sanitized
|
|
24
|
+
end
|
|
20
25
|
@app.call(env)
|
|
21
26
|
rescue SanitizedRackInput::FailedToReadBody
|
|
22
|
-
|
|
27
|
+
[400, { "content-type" => "text/plain" }, ["Bad Request"]]
|
|
23
28
|
end
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
DEFAULT_STRATEGIES = {
|
|
27
32
|
replace: lambda do |input|
|
|
28
|
-
input
|
|
29
|
-
force_encoding(Encoding::ASCII_8BIT)
|
|
30
|
-
encode!(Encoding::UTF_8,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
input
|
|
34
|
+
.force_encoding(Encoding::ASCII_8BIT)
|
|
35
|
+
.encode!(Encoding::UTF_8,
|
|
36
|
+
invalid: :replace,
|
|
37
|
+
undef: :replace)
|
|
33
38
|
input
|
|
34
39
|
end,
|
|
35
40
|
exception: lambda do |input|
|
|
36
|
-
input
|
|
37
|
-
force_encoding(Encoding::ASCII_8BIT)
|
|
38
|
-
encode!(Encoding::UTF_8)
|
|
41
|
+
input
|
|
42
|
+
.force_encoding(Encoding::ASCII_8BIT)
|
|
43
|
+
.encode!(Encoding::UTF_8)
|
|
39
44
|
input
|
|
40
45
|
end
|
|
41
46
|
}.freeze
|
|
@@ -73,8 +78,6 @@ module Rack
|
|
|
73
78
|
env[key] = sanitize_uri_encoded_string(value)
|
|
74
79
|
end
|
|
75
80
|
elsif key.to_s.start_with?("HTTP_")
|
|
76
|
-
# Just sanitize the headers and leave them in UTF-8. There is
|
|
77
|
-
# no reason to have UTF-8 in headers, but if it's valid, let it be.
|
|
78
81
|
if value.frozen?
|
|
79
82
|
env[key] = sanitize_string(value.dup).freeze
|
|
80
83
|
else
|
|
@@ -86,9 +89,7 @@ module Rack
|
|
|
86
89
|
|
|
87
90
|
private
|
|
88
91
|
|
|
89
|
-
def build_strategy(
|
|
90
|
-
strategy = options.fetch(:strategy) { :replace }
|
|
91
|
-
|
|
92
|
+
def build_strategy(strategy)
|
|
92
93
|
return strategy unless DEFAULT_STRATEGIES.key?(strategy)
|
|
93
94
|
|
|
94
95
|
DEFAULT_STRATEGIES[strategy]
|
|
@@ -128,7 +129,7 @@ module Rack
|
|
|
128
129
|
return unless env['HTTP_COOKIE']
|
|
129
130
|
|
|
130
131
|
env['HTTP_COOKIE'] = env['HTTP_COOKIE']
|
|
131
|
-
.split(
|
|
132
|
+
.split(/; */n)
|
|
132
133
|
.map { |cookie| sanitize_uri_encoded_string(cookie) }
|
|
133
134
|
.join('; ')
|
|
134
135
|
end
|
|
@@ -157,8 +158,8 @@ module Rack
|
|
|
157
158
|
|
|
158
159
|
def decode_string(input)
|
|
159
160
|
unescape_unreserved(
|
|
160
|
-
sanitize_string(input)
|
|
161
|
-
force_encoding(Encoding::ASCII_8BIT))
|
|
161
|
+
sanitize_string(input)
|
|
162
|
+
.force_encoding(Encoding::ASCII_8BIT))
|
|
162
163
|
end
|
|
163
164
|
|
|
164
165
|
# RFC3986, 2.2 states that the characters from 'reserved' group must be
|
|
@@ -202,14 +203,12 @@ module Rack
|
|
|
202
203
|
|
|
203
204
|
input.force_encoding(Encoding::UTF_8)
|
|
204
205
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@strategy.call(input)
|
|
209
|
-
end
|
|
210
|
-
else
|
|
211
|
-
input
|
|
206
|
+
input = @strategy.call(input) unless input.valid_encoding?
|
|
207
|
+
# Rack requires non-ASCII strings to use ASCII-8BIT encoding.
|
|
208
|
+
input = input.b if input.is_a?(String) && !input.ascii_only?
|
|
212
209
|
end
|
|
210
|
+
|
|
211
|
+
input
|
|
213
212
|
end
|
|
214
213
|
end
|
|
215
214
|
|
|
@@ -240,6 +239,10 @@ module Rack
|
|
|
240
239
|
sanitized_io.each(&block)
|
|
241
240
|
end
|
|
242
241
|
|
|
242
|
+
def ensure_sanitized
|
|
243
|
+
sanitized_io
|
|
244
|
+
end
|
|
245
|
+
|
|
243
246
|
def rewind
|
|
244
247
|
sanitized_io.rewind
|
|
245
248
|
end
|
data/rack-sanitizer.gemspec
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |gem|
|
|
4
4
|
gem.name = "rack-sanitizer"
|
|
5
|
-
gem.version = '2.0
|
|
6
|
-
gem.authors = ["Jean Boussier", "whitequark"]
|
|
5
|
+
gem.version = '2.1.0'
|
|
6
|
+
gem.authors = ["Shopify - Ruby and Rails Infratructure", "Jean Boussier", "whitequark"]
|
|
7
7
|
gem.license = "MIT"
|
|
8
|
-
gem.email = ["
|
|
8
|
+
gem.email = ["ruby-and-rails-infrastructure@shopify.com"]
|
|
9
9
|
gem.description = %{Rack::Sanitizer is a Rack middleware which cleans up } +
|
|
10
10
|
%{invalid UTF8 characters in request URI and headers.}
|
|
11
11
|
gem.summary = "It is a mordernized and optimized fork of rack-utf8_sanitizer"
|
|
12
12
|
gem.homepage = "http://github.com/Shopify/rack-sanitizer"
|
|
13
13
|
|
|
14
14
|
gem.files = `git ls-files`.split($/)
|
|
15
|
-
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
16
15
|
gem.require_paths = ["lib"]
|
|
17
16
|
|
|
18
17
|
gem.metadata["allowed_push_host"] = "https://rubygems.org/"
|
|
19
18
|
|
|
20
19
|
gem.required_ruby_version = '>= 2.5'
|
|
21
20
|
|
|
22
|
-
gem.add_dependency
|
|
23
|
-
|
|
24
|
-
gem.add_development_dependency "bacon"
|
|
25
|
-
gem.add_development_dependency "bacon-colored_output"
|
|
26
|
-
gem.add_development_dependency "rake"
|
|
21
|
+
gem.add_dependency "rack", '>= 1.0', '< 4.0'
|
|
27
22
|
end
|
data/test/test_sanitizer.rb
CHANGED
|
@@ -1,26 +1,70 @@
|
|
|
1
|
-
# encoding:ascii-8bit
|
|
1
|
+
# encoding: ascii-8bit
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require 'cgi'
|
|
5
|
+
require 'rack/lint'
|
|
6
|
+
require 'rack/mock'
|
|
4
7
|
require 'rack/sanitizer'
|
|
5
8
|
|
|
6
9
|
describe Rack::Sanitizer do
|
|
10
|
+
class ResponseBody < String
|
|
11
|
+
attr_reader :env
|
|
12
|
+
|
|
13
|
+
def initialize(env)
|
|
14
|
+
super('')
|
|
15
|
+
@env = env
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
7
19
|
before do
|
|
8
|
-
@
|
|
20
|
+
@build_app = ->(options = {}) do
|
|
21
|
+
linted_app = Rack::Lint.new(->env { [200, {}, [ResponseBody.new(env)]] })
|
|
22
|
+
Rack::Sanitizer.new(->env { linted_app.(env) }, options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@rack_env = ->(env) do
|
|
26
|
+
Rack::MockRequest.env_for('/').tap do |request_env|
|
|
27
|
+
request_env.delete('CONTENT_LENGTH')
|
|
28
|
+
request_env.merge!(env.reject { |_key, value| value.nil? })
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@call_app = ->(env) do
|
|
33
|
+
response = @app.(@rack_env.call(env))
|
|
34
|
+
response_body = nil
|
|
35
|
+
response[2].each do |chunk|
|
|
36
|
+
response_body = chunk
|
|
37
|
+
end
|
|
38
|
+
response[2].close
|
|
39
|
+
response_body.env
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@app = @build_app.call
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "conforms to Rack::Lint" do
|
|
46
|
+
app = Rack::Lint.new(Rack::Sanitizer.new(->(_env) { [200, {}, ["OK"]] }))
|
|
47
|
+
status, headers, body = app.(Rack::MockRequest.env_for('/'))
|
|
48
|
+
|
|
49
|
+
status.should == 200
|
|
50
|
+
headers.should == {}
|
|
51
|
+
body.each { |chunk| chunk.should == "OK" }
|
|
52
|
+
body.close
|
|
9
53
|
end
|
|
10
54
|
|
|
11
55
|
shared :does_sanitize_plain do
|
|
12
56
|
it "sanitizes plaintext entity (HTTP_USER_AGENT)" do
|
|
13
|
-
env = @
|
|
57
|
+
env = @call_app.call({ "HTTP_USER_AGENT" => @plain_input })
|
|
14
58
|
result = env["HTTP_USER_AGENT"]
|
|
15
59
|
|
|
16
|
-
result.encoding.should == Encoding::
|
|
60
|
+
result.encoding.should == Encoding::ASCII_8BIT
|
|
17
61
|
result.should.be.valid_encoding
|
|
18
62
|
end
|
|
19
63
|
end
|
|
20
64
|
|
|
21
65
|
shared :does_sanitize_uri do
|
|
22
66
|
it "sanitizes URI-like entity (REQUEST_PATH)" do
|
|
23
|
-
env = @
|
|
67
|
+
env = @call_app.call({ "REQUEST_PATH" => @uri_input })
|
|
24
68
|
result = env["REQUEST_PATH"]
|
|
25
69
|
|
|
26
70
|
result.encoding.should == Encoding::US_ASCII
|
|
@@ -31,7 +75,7 @@ describe Rack::Sanitizer do
|
|
|
31
75
|
describe "with invalid host input" do
|
|
32
76
|
it "sanitizes host entity (SERVER_NAME)" do
|
|
33
77
|
host = "host\xD0".dup.force_encoding(Encoding::UTF_8)
|
|
34
|
-
env = @
|
|
78
|
+
env = @call_app.call({ "SERVER_NAME" => host })
|
|
35
79
|
result = env["SERVER_NAME"]
|
|
36
80
|
|
|
37
81
|
result.encoding.should == Encoding::US_ASCII
|
|
@@ -77,18 +121,18 @@ describe Rack::Sanitizer do
|
|
|
77
121
|
|
|
78
122
|
shared :identity_plain do
|
|
79
123
|
it "does not change plaintext entity (HTTP_USER_AGENT)" do
|
|
80
|
-
env = @
|
|
124
|
+
env = @call_app.call({ "HTTP_USER_AGENT" => @plain_input })
|
|
81
125
|
result = env["HTTP_USER_AGENT"]
|
|
82
126
|
|
|
83
|
-
result.encoding.should == Encoding::UTF_8
|
|
127
|
+
result.encoding.should == (@plain_input.ascii_only? ? Encoding::UTF_8 : Encoding::ASCII_8BIT)
|
|
84
128
|
result.should.be.valid_encoding
|
|
85
|
-
result.should == @plain_input
|
|
129
|
+
result.bytes.should == @plain_input.bytes
|
|
86
130
|
end
|
|
87
131
|
end
|
|
88
132
|
|
|
89
133
|
shared :identity_uri do
|
|
90
134
|
it "does not change URI-like entity (REQUEST_PATH)" do
|
|
91
|
-
env = @
|
|
135
|
+
env = @call_app.call({ "REQUEST_PATH" => @uri_input })
|
|
92
136
|
result = env["REQUEST_PATH"]
|
|
93
137
|
|
|
94
138
|
result.encoding.should == Encoding::US_ASCII
|
|
@@ -122,7 +166,7 @@ describe Rack::Sanitizer do
|
|
|
122
166
|
end
|
|
123
167
|
|
|
124
168
|
it "does not change URI-like entity (REQUEST_PATH)" do
|
|
125
|
-
env = @
|
|
169
|
+
env = @call_app.call({ "REQUEST_PATH" => @uri_input })
|
|
126
170
|
result = env["REQUEST_PATH"]
|
|
127
171
|
|
|
128
172
|
result.encoding.should == Encoding::US_ASCII
|
|
@@ -151,13 +195,13 @@ describe Rack::Sanitizer do
|
|
|
151
195
|
|
|
152
196
|
describe "with frozen strings" do
|
|
153
197
|
before do
|
|
154
|
-
@plain_input = "bar baz"
|
|
155
|
-
@uri_input = "http://bar/bar+baz"
|
|
198
|
+
@plain_input = "bar baz"
|
|
199
|
+
@uri_input = "http://bar/bar+baz"
|
|
156
200
|
end
|
|
157
201
|
|
|
158
202
|
it "preserves the frozen? status of input" do
|
|
159
|
-
env = @
|
|
160
|
-
|
|
203
|
+
env = @call_app.call({ "HTTP_USER_AGENT" => @plain_input,
|
|
204
|
+
"REQUEST_PATH" => @uri_input })
|
|
161
205
|
|
|
162
206
|
env["HTTP_USER_AGENT"].should.be.frozen
|
|
163
207
|
env["REQUEST_PATH"].should.be.frozen
|
|
@@ -170,8 +214,8 @@ describe Rack::Sanitizer do
|
|
|
170
214
|
end
|
|
171
215
|
|
|
172
216
|
it "sanitizes REQUEST_PATH with invalid UTF-8 URI input" do
|
|
173
|
-
env
|
|
174
|
-
|
|
217
|
+
env = @call_app.call({ "requested_at" => "2014-07-22",
|
|
218
|
+
"REQUEST_PATH" => @uri_input })
|
|
175
219
|
|
|
176
220
|
result = env["REQUEST_PATH"]
|
|
177
221
|
|
|
@@ -193,17 +237,15 @@ describe Rack::Sanitizer do
|
|
|
193
237
|
|
|
194
238
|
def sanitize_form_data(request_env = request_env())
|
|
195
239
|
@uri_input = "http://bar/foo+%2F%3A+bar+%D0%BB%D0%BE%D0%BB".dup.force_encoding(Encoding::UTF_8)
|
|
196
|
-
@response_env = @
|
|
240
|
+
@response_env = @call_app.call(request_env)
|
|
197
241
|
sanitized_input = @response_env['rack.input'].read
|
|
198
242
|
|
|
199
243
|
yield sanitized_input if block_given?
|
|
200
244
|
|
|
201
|
-
@response_env['rack.input'].rewind
|
|
202
245
|
behaves_like :does_sanitize_plain
|
|
203
246
|
behaves_like :does_sanitize_uri
|
|
204
247
|
behaves_like :identity_plain
|
|
205
248
|
behaves_like :identity_uri
|
|
206
|
-
@response_env['rack.input'].close
|
|
207
249
|
end
|
|
208
250
|
|
|
209
251
|
class BrokenIO < StringIO
|
|
@@ -213,9 +255,18 @@ describe Rack::Sanitizer do
|
|
|
213
255
|
end
|
|
214
256
|
|
|
215
257
|
it "returns HTTP 400 on EOF" do
|
|
216
|
-
@rack_input = BrokenIO.new
|
|
217
|
-
|
|
218
|
-
|
|
258
|
+
@rack_input = BrokenIO.new("".b)
|
|
259
|
+
app = Rack::Lint.new(Rack::Sanitizer.new(->env {
|
|
260
|
+
env['rack.input'].read
|
|
261
|
+
[200, {}, ["OK"]]
|
|
262
|
+
}))
|
|
263
|
+
env = @rack_env.call(request_env.merge("HTTP_USER_AGENT" => "foo"))
|
|
264
|
+
status, headers, body = app.(env)
|
|
265
|
+
response_body = []
|
|
266
|
+
body.each { |chunk| response_body << chunk }
|
|
267
|
+
body.close
|
|
268
|
+
|
|
269
|
+
[status, headers, response_body].should == [400, { "content-type" => "text/plain" }, ["Bad Request"]]
|
|
219
270
|
end
|
|
220
271
|
|
|
221
272
|
it "sanitizes StringIO rack.input" do
|
|
@@ -241,7 +292,7 @@ describe Rack::Sanitizer do
|
|
|
241
292
|
end
|
|
242
293
|
|
|
243
294
|
it "sanitizes StringIO rack.input with bad encoding" do
|
|
244
|
-
input =
|
|
295
|
+
input = "foo=bla&quux=bar\xED"
|
|
245
296
|
@rack_input = StringIO.new input
|
|
246
297
|
|
|
247
298
|
sanitize_form_data do |sanitized_input|
|
|
@@ -292,7 +343,7 @@ describe Rack::Sanitizer do
|
|
|
292
343
|
|
|
293
344
|
it "sanitizes non-StringIO rack.input with bad encoding" do
|
|
294
345
|
require 'rack/rewindable_input'
|
|
295
|
-
input =
|
|
346
|
+
input = "foo=bla&quux=bar\xED"
|
|
296
347
|
@rack_input = Rack::RewindableInput.new(StringIO.new(input))
|
|
297
348
|
|
|
298
349
|
sanitize_form_data do |sanitized_input|
|
|
@@ -303,7 +354,7 @@ describe Rack::Sanitizer do
|
|
|
303
354
|
end
|
|
304
355
|
|
|
305
356
|
it "does not sanitize the rack body if there is no CONTENT_TYPE" do
|
|
306
|
-
input =
|
|
357
|
+
input = "foo=bla&quux=bar\xED"
|
|
307
358
|
@rack_input = StringIO.new input
|
|
308
359
|
|
|
309
360
|
env = request_env.update('CONTENT_TYPE' => nil)
|
|
@@ -315,7 +366,7 @@ describe Rack::Sanitizer do
|
|
|
315
366
|
end
|
|
316
367
|
|
|
317
368
|
it "does not sanitize the rack body if there is empty CONTENT_TYPE" do
|
|
318
|
-
input =
|
|
369
|
+
input = "foo=bla&quux=bar\xED"
|
|
319
370
|
@rack_input = StringIO.new input
|
|
320
371
|
|
|
321
372
|
env = request_env.update('CONTENT_TYPE' => '')
|
|
@@ -327,10 +378,10 @@ describe Rack::Sanitizer do
|
|
|
327
378
|
end
|
|
328
379
|
|
|
329
380
|
it "adjusts content-length when replacing input" do
|
|
330
|
-
input =
|
|
381
|
+
input = "foo=bla&quux=bar\xED"
|
|
331
382
|
@rack_input = StringIO.new input
|
|
332
383
|
|
|
333
|
-
env = request_env.update("CONTENT_LENGTH" => input.bytesize)
|
|
384
|
+
env = request_env.update("CONTENT_LENGTH" => input.bytesize.to_s)
|
|
334
385
|
sanitize_form_data(env) do |sanitized_input|
|
|
335
386
|
sanitized_input.bytesize.should != input.bytesize
|
|
336
387
|
@response_env["CONTENT_LENGTH"].should == sanitized_input.bytesize.to_s
|
|
@@ -338,7 +389,7 @@ describe Rack::Sanitizer do
|
|
|
338
389
|
end
|
|
339
390
|
|
|
340
391
|
it "does not sanitize null bytes by default" do
|
|
341
|
-
input =
|
|
392
|
+
input = "foo=bla&quux=bar%00"
|
|
342
393
|
@rack_input = StringIO.new input
|
|
343
394
|
|
|
344
395
|
sanitize_form_data do |sanitized_input|
|
|
@@ -361,18 +412,24 @@ describe Rack::Sanitizer do
|
|
|
361
412
|
|
|
362
413
|
it "sanitizes bad http cookie" do
|
|
363
414
|
@cookie = "foo=bla; quux=bar\xED"
|
|
364
|
-
response_env = @
|
|
415
|
+
response_env = @call_app.call(request_env)
|
|
365
416
|
response_env['HTTP_COOKIE'].should != @cookie
|
|
366
417
|
response_env['HTTP_COOKIE'].should == 'foo=bla; quux=bar%EF%BF%BD'
|
|
367
418
|
end
|
|
368
419
|
|
|
420
|
+
it "preserves commas in http cookie values" do
|
|
421
|
+
@cookie = "foo=bar,baz; quux=qux"
|
|
422
|
+
response_env = @call_app.call(request_env)
|
|
423
|
+
response_env['HTTP_COOKIE'].should == @cookie
|
|
424
|
+
end
|
|
425
|
+
|
|
369
426
|
it "does not change ok http cookie" do
|
|
370
427
|
@cookie = "foo=bla; quux=bar"
|
|
371
|
-
response_env = @
|
|
428
|
+
response_env = @call_app.call(request_env)
|
|
372
429
|
response_env['HTTP_COOKIE'].should == @cookie
|
|
373
430
|
|
|
374
431
|
@cookie = "foo=b%3bla; quux=b%20a%20r"
|
|
375
|
-
response_env = @
|
|
432
|
+
response_env = @call_app.call(request_env)
|
|
376
433
|
response_env['HTTP_COOKIE'].should == @cookie
|
|
377
434
|
end
|
|
378
435
|
end
|
|
@@ -390,14 +447,14 @@ describe Rack::Sanitizer do
|
|
|
390
447
|
|
|
391
448
|
def sanitize_data(request_env = request_env())
|
|
392
449
|
@uri_input = "http://bar/foo+%2F%3A+bar+%D0%BB%D0%BE%D0%BB".dup.force_encoding(Encoding::UTF_8)
|
|
393
|
-
@response_env = @
|
|
450
|
+
@response_env = @call_app.call(request_env)
|
|
394
451
|
sanitized_input = @response_env['rack.input'].read
|
|
395
452
|
|
|
396
453
|
yield sanitized_input if block_given?
|
|
397
454
|
end
|
|
398
455
|
|
|
399
456
|
it "does not sanitize custom content-type by default" do
|
|
400
|
-
input =
|
|
457
|
+
input = "foo=bla&quux=bar\xED"
|
|
401
458
|
@rack_input = StringIO.new input
|
|
402
459
|
|
|
403
460
|
env = request_env
|
|
@@ -409,47 +466,47 @@ describe Rack::Sanitizer do
|
|
|
409
466
|
end
|
|
410
467
|
|
|
411
468
|
it "sanitizes custom content-type if additional_content_types given" do
|
|
412
|
-
@app =
|
|
413
|
-
input =
|
|
469
|
+
@app = @build_app.call(additional_content_types: ["application/vnd.api+json"])
|
|
470
|
+
input = "foo=bla&quux=bar\xED"
|
|
414
471
|
@rack_input = StringIO.new input
|
|
415
472
|
|
|
416
473
|
env = request_env
|
|
417
474
|
sanitize_data(env) do |sanitized_input|
|
|
418
|
-
sanitized_input.encoding.should == Encoding::
|
|
475
|
+
sanitized_input.encoding.should == Encoding::ASCII_8BIT
|
|
419
476
|
sanitized_input.should.be.valid_encoding
|
|
420
477
|
sanitized_input.should != input
|
|
421
478
|
end
|
|
422
479
|
end
|
|
423
480
|
|
|
424
481
|
it "sanitizes default content-type if additional_content_types given" do
|
|
425
|
-
@app =
|
|
426
|
-
input =
|
|
482
|
+
@app = @build_app.call(additional_content_types: ["application/vnd.api+json"])
|
|
483
|
+
input = "foo=bla&quux=bar\xED"
|
|
427
484
|
@rack_input = StringIO.new input
|
|
428
485
|
|
|
429
486
|
env = request_env.update('CONTENT_TYPE' => 'application/json')
|
|
430
487
|
sanitize_data(env) do |sanitized_input|
|
|
431
|
-
sanitized_input.encoding.should == Encoding::
|
|
488
|
+
sanitized_input.encoding.should == Encoding::ASCII_8BIT
|
|
432
489
|
sanitized_input.should.be.valid_encoding
|
|
433
490
|
sanitized_input.should != input
|
|
434
491
|
end
|
|
435
492
|
end
|
|
436
493
|
|
|
437
494
|
it "sanitizes custom content-type if sanitizable_content_types given" do
|
|
438
|
-
@app =
|
|
439
|
-
input =
|
|
495
|
+
@app = @build_app.call(sanitizable_content_types: ["application/vnd.api+json"])
|
|
496
|
+
input = "foo=bla&quux=bar\xED"
|
|
440
497
|
@rack_input = StringIO.new input
|
|
441
498
|
|
|
442
499
|
env = request_env
|
|
443
500
|
sanitize_data(env) do |sanitized_input|
|
|
444
|
-
sanitized_input.encoding.should == Encoding::
|
|
501
|
+
sanitized_input.encoding.should == Encoding::ASCII_8BIT
|
|
445
502
|
sanitized_input.should.be.valid_encoding
|
|
446
503
|
sanitized_input.should != input
|
|
447
504
|
end
|
|
448
505
|
end
|
|
449
506
|
|
|
450
507
|
it "does not sanitize default content-type if sanitizable_content_types does not include it" do
|
|
451
|
-
@app =
|
|
452
|
-
input =
|
|
508
|
+
@app = @build_app.call(sanitizable_content_types: ["application/vnd.api+json"])
|
|
509
|
+
input = "foo=bla&quux=bar\xED"
|
|
453
510
|
@rack_input = StringIO.new input
|
|
454
511
|
|
|
455
512
|
env = request_env.update('CONTENT_TYPE' => 'application/json')
|
|
@@ -474,28 +531,28 @@ describe Rack::Sanitizer do
|
|
|
474
531
|
|
|
475
532
|
def sanitize_data(request_env = request_env())
|
|
476
533
|
@uri_input = "http://bar/foo+%2F%3A+bar+%D0%BB%D0%BE%D0%BB".dup.force_encoding(Encoding::UTF_8)
|
|
477
|
-
@response_env = @
|
|
534
|
+
@response_env = @call_app.call(request_env)
|
|
478
535
|
sanitized_input = @response_env['rack.input'].read
|
|
479
536
|
|
|
480
537
|
yield sanitized_input if block_given?
|
|
481
538
|
end
|
|
482
539
|
|
|
483
540
|
it "calls a default strategy (replace)" do
|
|
484
|
-
@app =
|
|
541
|
+
@app = @build_app.call
|
|
485
542
|
|
|
486
543
|
input = "foo=bla&quux=bar\xED"
|
|
487
544
|
@rack_input = StringIO.new input
|
|
488
545
|
|
|
489
546
|
env = request_env
|
|
490
547
|
sanitize_data(env) do |sanitized_input|
|
|
491
|
-
sanitized_input.encoding.should == Encoding::
|
|
548
|
+
sanitized_input.encoding.should == Encoding::ASCII_8BIT
|
|
492
549
|
sanitized_input.should.be.valid_encoding
|
|
493
550
|
sanitized_input.should != input
|
|
494
551
|
end
|
|
495
552
|
end
|
|
496
553
|
|
|
497
554
|
it "calls the exception strategy" do
|
|
498
|
-
@app =
|
|
555
|
+
@app = @build_app.call(strategy: :exception)
|
|
499
556
|
|
|
500
557
|
input = "foo=bla&quux=bar\xED"
|
|
501
558
|
@rack_input = StringIO.new input
|
|
@@ -504,12 +561,36 @@ describe Rack::Sanitizer do
|
|
|
504
561
|
should.raise(EncodingError) { sanitize_data(env) }
|
|
505
562
|
end
|
|
506
563
|
|
|
564
|
+
it "returns HTTP 400 before the application reads invalid rack.input" do
|
|
565
|
+
input = "{\"user_session\": {\"invalid\": \"\x80\"}}".b
|
|
566
|
+
@rack_input = StringIO.new(input)
|
|
567
|
+
|
|
568
|
+
sanitizer = Rack::Sanitizer.new(->(_env) { [200, {}, ["OK"]] }, strategy: :exception)
|
|
569
|
+
app = ->(env) do
|
|
570
|
+
begin
|
|
571
|
+
sanitizer.call(env)
|
|
572
|
+
rescue EncodingError
|
|
573
|
+
[400, {}, ["Bad Request"]]
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
app = Rack::Lint.new(app)
|
|
577
|
+
|
|
578
|
+
env = @rack_env.call(request_env.merge("HTTP_USER_AGENT" => "foo"))
|
|
579
|
+
env["CONTENT_LENGTH"] = input.bytesize.to_s
|
|
580
|
+
status, headers, body = app.(env)
|
|
581
|
+
response_body = []
|
|
582
|
+
body.each { |chunk| response_body << chunk }
|
|
583
|
+
body.close
|
|
584
|
+
|
|
585
|
+
[status, headers, response_body].should == [400, {}, ["Bad Request"]]
|
|
586
|
+
end
|
|
587
|
+
|
|
507
588
|
it "accepts a proc as a strategy" do
|
|
508
|
-
truncate = ->
|
|
589
|
+
truncate = ->(input) do
|
|
509
590
|
'replace'.dup.force_encoding(Encoding::UTF_8)
|
|
510
591
|
end
|
|
511
592
|
|
|
512
|
-
@app =
|
|
593
|
+
@app = @build_app.call(strategy: truncate)
|
|
513
594
|
|
|
514
595
|
input = "foo=bla&quux=bar\xED"
|
|
515
596
|
@rack_input = StringIO.new input
|
metadata
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rack-sanitizer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
+
- Shopify - Ruby and Rails Infratructure
|
|
7
8
|
- Jean Boussier
|
|
8
9
|
- whitequark
|
|
9
10
|
bindir: bin
|
|
10
11
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
13
|
dependencies:
|
|
13
14
|
- !ruby/object:Gem::Dependency
|
|
14
15
|
name: rack
|
|
@@ -30,52 +31,10 @@ dependencies:
|
|
|
30
31
|
- - "<"
|
|
31
32
|
- !ruby/object:Gem::Version
|
|
32
33
|
version: '4.0'
|
|
33
|
-
- !ruby/object:Gem::Dependency
|
|
34
|
-
name: bacon
|
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
40
|
-
type: :development
|
|
41
|
-
prerelease: false
|
|
42
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0'
|
|
47
|
-
- !ruby/object:Gem::Dependency
|
|
48
|
-
name: bacon-colored_output
|
|
49
|
-
requirement: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - ">="
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0'
|
|
54
|
-
type: :development
|
|
55
|
-
prerelease: false
|
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - ">="
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0'
|
|
61
|
-
- !ruby/object:Gem::Dependency
|
|
62
|
-
name: rake
|
|
63
|
-
requirement: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - ">="
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0'
|
|
68
|
-
type: :development
|
|
69
|
-
prerelease: false
|
|
70
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - ">="
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '0'
|
|
75
34
|
description: Rack::Sanitizer is a Rack middleware which cleans up invalid UTF8 characters
|
|
76
35
|
in request URI and headers.
|
|
77
36
|
email:
|
|
78
|
-
-
|
|
37
|
+
- ruby-and-rails-infrastructure@shopify.com
|
|
79
38
|
executables: []
|
|
80
39
|
extensions: []
|
|
81
40
|
extra_rdoc_files: []
|
|
@@ -84,7 +43,9 @@ files:
|
|
|
84
43
|
- ".github/dependabot.yml"
|
|
85
44
|
- ".github/workflows/ci.yml"
|
|
86
45
|
- ".github/workflows/cla.yml"
|
|
46
|
+
- ".github/workflows/release.yml"
|
|
87
47
|
- ".gitignore"
|
|
48
|
+
- ".rubocop.yml"
|
|
88
49
|
- ".ruby-version"
|
|
89
50
|
- Gemfile
|
|
90
51
|
- Gemfile.lock
|
|
@@ -92,6 +53,12 @@ files:
|
|
|
92
53
|
- README.md
|
|
93
54
|
- Rakefile
|
|
94
55
|
- bin/bacon
|
|
56
|
+
- bin/rubocop
|
|
57
|
+
- dev.yml
|
|
58
|
+
- gemfiles/rack_2.2.gemfile
|
|
59
|
+
- gemfiles/rack_3.0.gemfile
|
|
60
|
+
- gemfiles/rack_3.1.gemfile
|
|
61
|
+
- gemfiles/rack_3.2.gemfile
|
|
95
62
|
- lib/rack/sanitizer.rb
|
|
96
63
|
- rack-sanitizer.gemspec
|
|
97
64
|
- test/test_sanitizer.rb
|
|
@@ -114,8 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
114
81
|
- !ruby/object:Gem::Version
|
|
115
82
|
version: '0'
|
|
116
83
|
requirements: []
|
|
117
|
-
rubygems_version:
|
|
84
|
+
rubygems_version: 4.0.19
|
|
118
85
|
specification_version: 4
|
|
119
86
|
summary: It is a mordernized and optimized fork of rack-utf8_sanitizer
|
|
120
|
-
test_files:
|
|
121
|
-
- test/test_sanitizer.rb
|
|
87
|
+
test_files: []
|