rack-protection 3.2.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7640a15f8659807abd53474e7ce538a42e476e4bd99dc745f3b9b8c16161c008
4
- data.tar.gz: '05468ec6c8113d3afce2df62221e4c866616999700c30ba3ef94a2705b11138b'
3
+ metadata.gz: 8762f8b7bc260c94e353c6c9eb9a24d3a0efe695cb5904f526a5a84094f567db
4
+ data.tar.gz: d3c7e05bf8bfea7c6b077025f330429f386b416bccce5ebfab2d91d0513e437b
5
5
  SHA512:
6
- metadata.gz: eeaff5e584a8ee3be6c80dc92c67fcc95bdbb97b084509ed90ca9ad524598fba63690cfa372586edd27940cd609fa44210637e9c95fbf1191e1a5cc297f222ac
7
- data.tar.gz: 26e2160d65b6015c7aaa52266b7241d15f645eb259d1371b864b3e2b6a3b1fbef841e62304bc8e39a83fab1a52ddb0c3455a51385a9a451c833cbed91b75d00a
6
+ metadata.gz: 48b9fffade45349249d1122c3acc8618f2cb8ca05cde4ce1959b063899faad7c5c433bf6481518beedd0a0dd2ff28c878b1f5898fc3f21629ea39233fa747dc6
7
+ data.tar.gz: ec3176d39b37dfdd97b4d230f98251681a93c6dddcb17db4659c998d8f296dc9fc97808a9b732bbf055d0d2b0504f9696f541b6d6864983b214df94ceff636a5
data/Gemfile CHANGED
@@ -1,16 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  source 'https://rubygems.org'
4
- # encoding: utf-8
4
+ gemspec
5
5
 
6
6
  gem 'rake'
7
7
  gem 'rspec', '~> 3'
8
+ gem 'rack-test'
8
9
 
9
10
  rack_version = ENV['rack'].to_s
10
11
  rack_version = nil if rack_version.empty? || (rack_version == 'stable')
11
12
  rack_version = { github: 'rack/rack' } if rack_version == 'head'
12
13
  gem 'rack', rack_version
13
-
14
- gemspec
15
-
16
- gem 'rack-test'
data/README.md CHANGED
@@ -69,7 +69,7 @@ Prevented by:
69
69
 
70
70
  Prevented by:
71
71
 
72
- * [`Rack::Protection::SessionHijacking`][session-hijacking]
72
+ * [`Rack::Protection::SessionHijacking`][session-hijacking] (not included by `use Rack::Protection`)
73
73
 
74
74
  ## Cookie Tossing
75
75
 
@@ -51,6 +51,7 @@ module Rack
51
51
  # Here is <tt>server.rb</tt>:
52
52
  #
53
53
  # require 'rack/protection'
54
+ # require 'rack/session'
54
55
  #
55
56
  # app = Rack::Builder.app do
56
57
  # use Rack::Session::Cookie, secret: 'secret'
@@ -74,7 +74,7 @@ module Rack
74
74
 
75
75
  def deny(env)
76
76
  warn env, "attack prevented by #{self.class}"
77
- [options[:status], { 'Content-Type' => 'text/plain' }, [options[:message]]]
77
+ [options[:status], { 'content-type' => 'text/plain' }, [options[:message]]]
78
78
  end
79
79
 
80
80
  def report(env)
@@ -26,7 +26,7 @@ module Rack
26
26
  # https://scotthelme.co.uk/csp-cheat-sheet/
27
27
  # http://www.html5rocks.com/en/tutorials/security/content-security-policy/
28
28
  #
29
- # Sets the 'Content-Security-Policy[-Report-Only]' header.
29
+ # Sets the 'content-security-policy[-report-only]' header.
30
30
  #
31
31
  # Options: ContentSecurityPolicy configuration is a complex topic with
32
32
  # several levels of support that has evolved over time.
@@ -71,7 +71,7 @@ module Rack
71
71
 
72
72
  def call(env)
73
73
  status, headers, body = @app.call(env)
74
- header = options[:report_only] ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
74
+ header = options[:report_only] ? 'content-security-policy-report-only' : 'content-security-policy'
75
75
  headers[header] ||= csp_policy if html? headers
76
76
  [status, headers, body]
77
77
  end
@@ -51,7 +51,7 @@ module Rack
51
51
  def redirect(env)
52
52
  request = Request.new(env)
53
53
  warn env, "attack prevented by #{self.class}"
54
- [302, { 'Content-Type' => 'text/html', 'Location' => request.path }, []]
54
+ [302, { 'content-type' => 'text/html', 'location' => request.path }, []]
55
55
  end
56
56
 
57
57
  def bad_cookies
@@ -31,7 +31,7 @@ module Rack
31
31
 
32
32
  def call(env)
33
33
  status, headers, body = @app.call(env)
34
- headers['X-Frame-Options'] ||= frame_options if html? headers
34
+ headers['x-frame-options'] ||= frame_options if html? headers
35
35
  [status, headers, body]
36
36
  end
37
37
  end
@@ -39,7 +39,7 @@ module Rack
39
39
  def has_vector?(request, headers)
40
40
  return false if request.xhr?
41
41
  return false if options[:allow_if]&.call(request.env)
42
- return false unless headers['Content-Type'].to_s.split(';', 2).first =~ %r{^\s*application/json\s*$}
42
+ return false unless headers['content-type'].to_s.split(';', 2).first =~ %r{^\s*application/json\s*$}
43
43
 
44
44
  origin(request.env).nil? and referrer(request.env) != request.host
45
45
  end
@@ -19,7 +19,7 @@ module Rack
19
19
 
20
20
  def call(env)
21
21
  status, headers, body = @app.call(env)
22
- headers['Referrer-Policy'] ||= options[:referrer_policy]
22
+ headers['referrer-policy'] ||= options[:referrer_policy]
23
23
  [status, headers, body]
24
24
  end
25
25
  end
@@ -33,7 +33,7 @@ module Rack
33
33
 
34
34
  def call(env)
35
35
  status, headers, body = @app.call(env)
36
- headers['Strict-Transport-Security'] ||= strict_transport
36
+ headers['strict-transport-security'] ||= strict_transport
37
37
  [status, headers, body]
38
38
  end
39
39
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  module Protection
5
- VERSION = '3.2.0'
5
+ VERSION = '4.0.0'
6
6
  end
7
7
  end
@@ -18,8 +18,8 @@ module Rack
18
18
 
19
19
  def call(env)
20
20
  status, headers, body = @app.call(env)
21
- headers['X-XSS-Protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers
22
- headers['X-Content-Type-Options'] ||= 'nosniff' if options[:nosniff]
21
+ headers['x-xss-protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers
22
+ headers['x-content-type-options'] ||= 'nosniff' if options[:nosniff]
23
23
  [status, headers, body]
24
24
  end
25
25
  end
@@ -9,8 +9,6 @@ module Rack
9
9
  autoload :Base, 'rack/protection/base'
10
10
  autoload :CookieTossing, 'rack/protection/cookie_tossing'
11
11
  autoload :ContentSecurityPolicy, 'rack/protection/content_security_policy'
12
- autoload :Encryptor, 'rack/protection/encryptor'
13
- autoload :EncryptedCookie, 'rack/protection/encrypted_cookie'
14
12
  autoload :EscapedParams, 'rack/protection/escaped_params'
15
13
  autoload :FormToken, 'rack/protection/form_token'
16
14
  autoload :FrameOptions, 'rack/protection/frame_options'
@@ -26,12 +24,11 @@ module Rack
26
24
  autoload :XSSHeader, 'rack/protection/xss_header'
27
25
 
28
26
  def self.new(app, options = {})
29
- # does not include: RemoteReferrer, AuthenticityToken and FormToken
30
27
  except = Array options[:except]
31
28
  use_these = Array options[:use]
32
29
 
33
30
  if options.fetch(:without_session, false)
34
- except += %i[session_hijacking remote_token]
31
+ except += %i[remote_token]
35
32
  end
36
33
 
37
34
  Rack::Builder.new do
@@ -43,6 +40,7 @@ module Rack
43
40
  use ::Rack::Protection::FormToken, options if use_these.include? :form_token
44
41
  use ::Rack::Protection::ReferrerPolicy, options if use_these.include? :referrer_policy
45
42
  use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
43
+ use ::Rack::Protection::SessionHijacking, options if use_these.include? :session_hijacking
46
44
  use ::Rack::Protection::StrictTransport, options if use_these.include? :strict_transport
47
45
 
48
46
  # On by default, unless skipped
@@ -52,7 +50,6 @@ module Rack
52
50
  use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
53
51
  use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
54
52
  use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
55
- use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
56
53
  use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
57
54
  run app
58
55
  end.to_app
@@ -36,9 +36,9 @@ RubyGems 2.0 or newer is required to protect against public gem pushes. You can
36
36
  'rubygems_mfa_required' => 'true'
37
37
  }
38
38
 
39
- s.required_ruby_version = '>= 2.6.0'
39
+ s.required_ruby_version = '>= 2.7.8'
40
40
 
41
41
  # dependencies
42
42
  s.add_dependency 'base64', '>= 0.1.0'
43
- s.add_dependency 'rack', '~> 2.2', '>= 2.2.4'
43
+ s.add_dependency 'rack', '>= 3.0.0', '< 4'
44
44
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-protection
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - https://github.com/sinatra/sinatra/graphs/contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-29 00:00:00.000000000 Z
11
+ date: 2024-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -28,22 +28,22 @@ dependencies:
28
28
  name: rack
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '2.2'
34
31
  - - ">="
35
32
  - !ruby/object:Gem::Version
36
- version: 2.2.4
33
+ version: 3.0.0
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '4'
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
- - - "~>"
42
- - !ruby/object:Gem::Version
43
- version: '2.2'
44
41
  - - ">="
45
42
  - !ruby/object:Gem::Version
46
- version: 2.2.4
43
+ version: 3.0.0
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '4'
47
47
  description: Protect against typical web attacks, works with all Rack apps, including
48
48
  Rails
49
49
  email: sinatrarb@googlegroups.com
@@ -61,8 +61,6 @@ files:
61
61
  - lib/rack/protection/base.rb
62
62
  - lib/rack/protection/content_security_policy.rb
63
63
  - lib/rack/protection/cookie_tossing.rb
64
- - lib/rack/protection/encrypted_cookie.rb
65
- - lib/rack/protection/encryptor.rb
66
64
  - lib/rack/protection/escaped_params.rb
67
65
  - lib/rack/protection/form_token.rb
68
66
  - lib/rack/protection/frame_options.rb
@@ -95,7 +93,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
95
93
  requirements:
96
94
  - - ">="
97
95
  - !ruby/object:Gem::Version
98
- version: 2.6.0
96
+ version: 2.7.8
99
97
  required_rubygems_version: !ruby/object:Gem::Requirement
100
98
  requirements:
101
99
  - - ">="
@@ -1,273 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'openssl'
4
- require 'zlib'
5
- require 'json'
6
- require 'rack/request'
7
- require 'rack/response'
8
- require 'rack/session/abstract/id'
9
-
10
- module Rack
11
- module Protection
12
- # Rack::Protection::EncryptedCookie provides simple cookie based session management.
13
- # By default, the session is a Ruby Hash stored as base64 encoded marshalled
14
- # data set to :key (default: rack.session). The object that encodes the
15
- # session data is configurable and must respond to +encode+ and +decode+.
16
- # Both methods must take a string and return a string.
17
- #
18
- # When the secret key is set, cookie data is checked for data integrity.
19
- # The old_secret key is also accepted and allows graceful secret rotation.
20
- # A legacy_hmac_secret is also accepted and is used to upgrade existing
21
- # sessions to the new encryption scheme.
22
- #
23
- # There is also a legacy_hmac_coder option which can be set if a non-default
24
- # coder was used for legacy session cookies.
25
- #
26
- # Example:
27
- #
28
- # use Rack::Protection::EncryptedCookie,
29
- # :key => 'rack.session',
30
- # :domain => 'foo.com',
31
- # :path => '/',
32
- # :expire_after => 2592000,
33
- # :secret => 'change_me',
34
- # :old_secret => 'old_secret'
35
- #
36
- # All parameters are optional.
37
- #
38
- # Example using legacy HMAC options
39
- #
40
- # Rack::Protection:EncryptedCookie.new(application, {
41
- # # The secret used for legacy HMAC cookies
42
- # legacy_hmac_secret: 'legacy secret',
43
- # # legacy_hmac_coder will default to Rack::Protection::EncryptedCookie::Base64::Marshal
44
- # legacy_hmac_coder: Rack::Protection::EncryptedCookie::Identity.new,
45
- # # legacy_hmac will default to OpenSSL::Digest::SHA1
46
- # legacy_hmac: OpenSSL::Digest::SHA256
47
- # })
48
- #
49
- # Example of a cookie with no encoding:
50
- #
51
- # Rack::Protection::EncryptedCookie.new(application, {
52
- # :coder => Rack::Protection::EncryptedCookie::Identity.new
53
- # })
54
- #
55
- # Example of a cookie with custom encoding:
56
- #
57
- # Rack::Protection::EncryptedCookie.new(application, {
58
- # :coder => Class.new {
59
- # def encode(str); str.reverse; end
60
- # def decode(str); str.reverse; end
61
- # }.new
62
- # })
63
- #
64
- class EncryptedCookie < Rack::Session::Abstract::Persisted
65
- # Encode session cookies as Base64
66
- class Base64
67
- def encode(str)
68
- [str].pack('m0')
69
- end
70
-
71
- def decode(str)
72
- str.unpack1('m')
73
- end
74
-
75
- # Encode session cookies as Marshaled Base64 data
76
- class Marshal < Base64
77
- def encode(str)
78
- super(::Marshal.dump(str))
79
- end
80
-
81
- def decode(str)
82
- return unless str
83
-
84
- begin
85
- ::Marshal.load(super(str))
86
- rescue StandardError
87
- nil
88
- end
89
- end
90
- end
91
-
92
- # N.B. Unlike other encoding methods, the contained objects must be a
93
- # valid JSON composite type, either a Hash or an Array.
94
- class JSON < Base64
95
- def encode(obj)
96
- super(::JSON.dump(obj))
97
- end
98
-
99
- def decode(str)
100
- return unless str
101
-
102
- begin
103
- ::JSON.parse(super(str))
104
- rescue StandardError
105
- nil
106
- end
107
- end
108
- end
109
-
110
- class ZipJSON < Base64
111
- def encode(obj)
112
- super(Zlib::Deflate.deflate(::JSON.dump(obj)))
113
- end
114
-
115
- def decode(str)
116
- return unless str
117
-
118
- ::JSON.parse(Zlib::Inflate.inflate(super(str)))
119
- rescue StandardError
120
- nil
121
- end
122
- end
123
- end
124
-
125
- # Use no encoding for session cookies
126
- class Identity
127
- def encode(str); str; end
128
- def decode(str); str; end
129
- end
130
-
131
- class Marshal
132
- def encode(str)
133
- ::Marshal.dump(str)
134
- end
135
-
136
- def decode(str)
137
- ::Marshal.load(str) if str
138
- end
139
- end
140
-
141
- attr_reader :coder
142
-
143
- def initialize(app, options = {})
144
- # Assume keys are hex strings and convert them to raw byte strings for
145
- # actual key material
146
- @secrets = options.values_at(:secret, :old_secret).compact.map do |secret|
147
- [secret].pack('H*')
148
- end
149
-
150
- warn <<-MSG unless secure?(options)
151
- SECURITY WARNING: No secret option provided to Rack::Protection::EncryptedCookie.
152
- This poses a security threat. It is strongly recommended that you
153
- provide a secret to prevent exploits that may be possible from crafted
154
- cookies. This will not be supported in future versions of Rack, and
155
- future versions will even invalidate your existing user cookies.
156
-
157
- Called from: #{caller[0]}.
158
- MSG
159
-
160
- warn <<-MSG if @secrets.first && @secrets.first.length < 32
161
- SECURITY WARNING: Your secret is not long enough. It must be at least
162
- 32 bytes long and securely random. To generate such a key for use
163
- you can run the following command:
164
-
165
- ruby -rsecurerandom -e 'p SecureRandom.hex(32)'
166
-
167
- Called from: #{caller[0]}.
168
- MSG
169
-
170
- if options.key?(:legacy_hmac_secret)
171
- @legacy_hmac = options.fetch(:legacy_hmac, OpenSSL::Digest::SHA1)
172
-
173
- # Multiply the :digest_length: by 2 because this value is the length of
174
- # the digest in bytes but session digest strings are encoded as hex
175
- # strings
176
- @legacy_hmac_length = @legacy_hmac.new.digest_length * 2
177
- @legacy_hmac_secret = options[:legacy_hmac_secret]
178
- @legacy_hmac_coder = (options[:legacy_hmac_coder] ||= Base64::Marshal.new)
179
- else
180
- @legacy_hmac = false
181
- end
182
-
183
- # If encryption is used we can just use a default Marshal encoder
184
- # without Base64 encoding the results.
185
- #
186
- # If no encryption is used, rely on the previous default (Base64::Marshal)
187
- @coder = (options[:coder] ||= (@secrets.any? ? Marshal.new : Base64::Marshal.new))
188
-
189
- super(app, options.merge!(cookie_only: true))
190
- end
191
-
192
- private
193
-
194
- def find_session(req, _sid)
195
- data = unpacked_cookie_data(req)
196
- data = persistent_session_id!(data)
197
- [data['session_id'], data]
198
- end
199
-
200
- def extract_session_id(request)
201
- unpacked_cookie_data(request)['session_id']
202
- end
203
-
204
- def unpacked_cookie_data(request)
205
- request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
206
- session_data = cookie_data = request.cookies[@key]
207
-
208
- # Try to decrypt with the first secret, if that returns nil, try
209
- # with old_secret
210
- unless @secrets.empty?
211
- session_data = Rack::Protection::Encryptor.decrypt_message(cookie_data, @secrets.first)
212
- session_data ||= Rack::Protection::Encryptor.decrypt_message(cookie_data, @secrets[1]) if @secrets.size > 1
213
- end
214
-
215
- # If session_data is still nil, are there is a legacy HMAC
216
- # configured, try verify and parse the cookie that way
217
- if !session_data && @legacy_hmac
218
- digest = cookie_data.slice!(-@legacy_hmac_length..-1)
219
- cookie_data.slice!(-2..-1) # remove double dash
220
- session_data = cookie_data if digest_match?(cookie_data, digest)
221
-
222
- # Decode using legacy HMAC decoder
223
- request.set_header(k, @legacy_hmac_coder.decode(session_data) || {})
224
- else
225
- request.set_header(k, coder.decode(session_data) || {})
226
- end
227
- end
228
- end
229
-
230
- def persistent_session_id!(data, sid = nil)
231
- data ||= {}
232
- data['session_id'] ||= sid || generate_sid
233
- data
234
- end
235
-
236
- def write_session(req, session_id, session, _options)
237
- session = session.merge('session_id' => session_id)
238
- session_data = coder.encode(session)
239
-
240
- unless @secrets.empty?
241
- session_data = Rack::Protection::Encryptor.encrypt_message(session_data, @secrets.first)
242
- end
243
-
244
- if session_data.size > (4096 - @key.size)
245
- req.get_header(RACK_ERRORS).puts('Warning! Rack::Protection::EncryptedCookie data size exceeds 4K.')
246
- nil
247
- else
248
- session_data
249
- end
250
- end
251
-
252
- def delete_session(_req, _session_id, options)
253
- # Nothing to do here, data is in the client
254
- generate_sid unless options[:drop]
255
- end
256
-
257
- def digest_match?(data, digest)
258
- return false unless data && digest
259
-
260
- Rack::Utils.secure_compare(digest, generate_hmac(data))
261
- end
262
-
263
- def generate_hmac(data)
264
- OpenSSL::HMAC.hexdigest(@legacy_hmac.new, @legacy_hmac_secret, data)
265
- end
266
-
267
- def secure?(options)
268
- @secrets.size >= 1 ||
269
- (options[:coder] && options[:let_coder_handle_secure_encoding])
270
- end
271
- end
272
- end
273
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'openssl'
4
-
5
- module Rack
6
- module Protection
7
- module Encryptor
8
- CIPHER = 'aes-256-gcm'
9
- DELIMITER = '--'
10
-
11
- def self.base64_encode(str)
12
- [str].pack('m0')
13
- end
14
-
15
- def self.base64_decode(str)
16
- str.unpack1('m0')
17
- end
18
-
19
- def self.encrypt_message(data, secret, auth_data = '')
20
- raise ArgumentError, 'data cannot be nil' if data.nil?
21
-
22
- cipher = OpenSSL::Cipher.new(CIPHER)
23
- cipher.encrypt
24
- cipher.key = secret[0, cipher.key_len]
25
-
26
- # Rely on OpenSSL for the initialization vector
27
- iv = cipher.random_iv
28
-
29
- # This must be set to properly use AES GCM for the OpenSSL module
30
- cipher.auth_data = auth_data
31
-
32
- cipher_text = cipher.update(data)
33
- cipher_text << cipher.final
34
-
35
- "#{base64_encode cipher_text}#{DELIMITER}#{base64_encode iv}#{DELIMITER}#{base64_encode cipher.auth_tag}"
36
- end
37
-
38
- def self.decrypt_message(data, secret)
39
- return unless data
40
-
41
- cipher = OpenSSL::Cipher.new(CIPHER)
42
- cipher_text, iv, auth_tag = data.split(DELIMITER, 3).map! { |v| base64_decode(v) }
43
-
44
- # This check is from ActiveSupport::MessageEncryptor
45
- # see: https://github.com/ruby/openssl/issues/63
46
- return if auth_tag.nil? || auth_tag.bytes.length != 16
47
-
48
- cipher.decrypt
49
- cipher.key = secret[0, cipher.key_len]
50
- cipher.iv = iv
51
- cipher.auth_tag = auth_tag
52
- cipher.auth_data = ''
53
-
54
- decrypted_data = cipher.update(cipher_text)
55
- decrypted_data << cipher.final
56
- decrypted_data
57
- rescue OpenSSL::Cipher::CipherError, TypeError, ArgumentError
58
- nil
59
- end
60
- end
61
- end
62
- end