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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df0d39073134888976a11608a842c3c5de86b8bde87c8d157b133a99a7278253
4
- data.tar.gz: 233aeb1b01a1766196206d2ce51add45b52eb1bea05565716ac46ddca27704e8
3
+ metadata.gz: fca030fbcd503f8e7a4f61ebd1286552baf24d3b1a8607af3c02ca12dd9533e0
4
+ data.tar.gz: 44770acabfcea4af68db609520fd158986c72e5a033151352384a08f16174cda
5
5
  SHA512:
6
- metadata.gz: 378f8675ec6d51e81cdd43b25adbe7ce2bfd02420f0d0da2f953d47d15ebd58bca9373517403d8e6c5e86f4a038e9b41a76519fcf10c26718dbac7185f00b06f
7
- data.tar.gz: b108ed7404f5aeed13fc1582d1916fd3d21248c69cde05121225e1adccf2f26c1b3298def5f27a99f601fc665758c164b83fd91eca315674859f81b67db85e4a
6
+ metadata.gz: 0a88c1f9a8f7aa00cf4b7a6c2ad9306f53218d467177bc154efcb12a9cebf7c2bb79a7404dbd0c1a813929b426a5169d714149488e92318ce4b0b0c80067971e
7
+ data.tar.gz: 0fc65bc666ac425913aa02abb117545e29dc193961e41a91cf4f704be14567f2271e8e028527a3e0872c2d129029500880959a3058d36eedd3150b079fc4d781
@@ -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.2, jruby-9.3, jruby-head]
14
- include:
15
- - ruby: "3.4"
16
- rubyopt: "--enable-frozen-string-literal --debug-frozen-string-literal"
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@v4
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
@@ -1,4 +1,5 @@
1
1
  *.gem
2
+ /gemfiles/*.lock
2
3
  *.rbc
3
4
  .bundle
4
5
  .config
data/.rubocop.yml ADDED
@@ -0,0 +1,12 @@
1
+
2
+ inherit_gem:
3
+ rubocop-shopify: rubocop.yml
4
+
5
+ AllCops:
6
+ SuggestExtensions: false
7
+ TargetRubyVersion: 2.5
8
+ Exclude:
9
+ - bin/bacon
10
+
11
+ Lint/Void:
12
+ Enabled: false
data/Gemfile CHANGED
@@ -1,4 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in rack-sanitizer.gemspec
4
6
  gemspec
7
+
8
+ gem 'rubocop-shopify'
9
+ gem 'rake'
10
+ gem 'ostruct'
data/Gemfile.lock CHANGED
@@ -1,26 +1,57 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rack-sanitizer (2.0.4)
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
- bacon (1.2.0)
11
- bacon-colored_output (1.1.1)
12
- bacon
13
- rack (3.0.10)
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
- bacon
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 and the (UTF-8 encoded) result is left in the environment.
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
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
 
3
- task :default => :spec
5
+ task default: :spec
4
6
 
5
7
  desc "Run tests"
6
8
  task :spec do
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
@@ -0,0 +1,17 @@
1
+ name: rack-sanitizer
2
+
3
+ type: ruby
4
+ nix: true
5
+
6
+ up:
7
+ - ruby
8
+ - bundler
9
+
10
+ commands:
11
+ test:
12
+ syntax:
13
+ argument: file
14
+ optional: args...
15
+ run: bundle exec rake spec
16
+ style:
17
+ run: bin/rubocop
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+ gem "rubocop-shopify"
7
+ gem "rake"
8
+ gem "ostruct"
9
+ gem "rack", "~> 2.2.0"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+ gem "rubocop-shopify"
7
+ gem "rake"
8
+ gem "ostruct"
9
+ gem "rack", "~> 3.0.0"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+ gem "rubocop-shopify"
7
+ gem "rake"
8
+ gem "ostruct"
9
+ gem "rack", "~> 3.1.0"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+ gem "rubocop-shopify"
7
+ gem "rake"
8
+ gem "ostruct"
9
+ gem "rack", "~> 3.2.0"
@@ -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
- @strategy = build_strategy(options)
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
- return [400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
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
- invalid: :replace,
32
- undef: :replace)
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(options)
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(/[;,] */n)
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
- if input.valid_encoding?
206
- input
207
- else
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
@@ -1,27 +1,22 @@
1
- # -*- encoding: utf-8 -*-
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.4'
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 = ["jean.boussier@gmail.org"]
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 "rack", '>= 1.0', '< 4.0'
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
@@ -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
- @app = Rack::Sanitizer.new(-> env { env["rack.input"]&.size; env })
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 = @app.({ "HTTP_USER_AGENT" => @plain_input })
57
+ env = @call_app.call({ "HTTP_USER_AGENT" => @plain_input })
14
58
  result = env["HTTP_USER_AGENT"]
15
59
 
16
- result.encoding.should == Encoding::UTF_8
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 = @app.({ "REQUEST_PATH" => @uri_input })
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 = @app.({ "SERVER_NAME" => host })
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 = @app.({ "HTTP_USER_AGENT" => @plain_input })
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 = @app.({ "REQUEST_PATH" => @uri_input })
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 = @app.({ "REQUEST_PATH" => @uri_input })
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".freeze
155
- @uri_input = "http://bar/bar+baz".freeze
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 = @app.({ "HTTP_USER_AGENT" => @plain_input,
160
- "REQUEST_PATH" => @uri_input })
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 = @app.({ :requested_at => "2014-07-22",
174
- "REQUEST_PATH" => @uri_input })
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 = @app.(request_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
- @response_env = @app.(request_env)
218
- @response_env.should == [400, {"Content-Type"=>"text/plain"}, ["Bad Request"]]
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 = "foo=bla&quux=bar\xED"
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 = "foo=bla&quux=bar\xED"
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 = "foo=bla&quux=bar\xED"
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 = "foo=bla&quux=bar\xED"
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 = "foo=bla&quux=bar\xED"
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 = "foo=bla&quux=bar%00"
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 = @app.(request_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 = @app.(request_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 = @app.(request_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 = @app.(request_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 = "foo=bla&quux=bar\xED"
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 = Rack::Sanitizer.new(-> env { env }, additional_content_types: ["application/vnd.api+json"])
413
- input = "foo=bla&quux=bar\xED"
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::UTF_8
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 = Rack::Sanitizer.new(-> env { env }, additional_content_types: ["application/vnd.api+json"])
426
- input = "foo=bla&quux=bar\xED"
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::UTF_8
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 = Rack::Sanitizer.new(-> env { env }, sanitizable_content_types: ["application/vnd.api+json"])
439
- input = "foo=bla&quux=bar\xED"
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::UTF_8
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 = Rack::Sanitizer.new(-> env { env }, sanitizable_content_types: ["application/vnd.api+json"])
452
- input = "foo=bla&quux=bar\xED"
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 = @app.(request_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 = Rack::Sanitizer.new(-> env { env })
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::UTF_8
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 = Rack::Sanitizer.new(-> env { env }, strategy: :exception)
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 = -> (input) do
589
+ truncate = ->(input) do
509
590
  'replace'.dup.force_encoding(Encoding::UTF_8)
510
591
  end
511
592
 
512
- @app = Rack::Sanitizer.new(-> env { env }, strategy: truncate)
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
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: 2025-03-12 00:00:00.000000000 Z
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
- - jean.boussier@gmail.org
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: 3.6.5
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: []