rack-protection 3.2.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7640a15f8659807abd53474e7ce538a42e476e4bd99dc745f3b9b8c16161c008
4
- data.tar.gz: '05468ec6c8113d3afce2df62221e4c866616999700c30ba3ef94a2705b11138b'
3
+ metadata.gz: 98553e74b3c41ed1630ad332c07ac4be4d2cf7f9f7d42c4ea383ed10b36cfda8
4
+ data.tar.gz: d1af8ebd50af6447a3879ae531de1dd5e664cb7493919f2af94c4d2a5d0036d4
5
5
  SHA512:
6
- metadata.gz: eeaff5e584a8ee3be6c80dc92c67fcc95bdbb97b084509ed90ca9ad524598fba63690cfa372586edd27940cd609fa44210637e9c95fbf1191e1a5cc297f222ac
7
- data.tar.gz: 26e2160d65b6015c7aaa52266b7241d15f645eb259d1371b864b3e2b6a3b1fbef841e62304bc8e39a83fab1a52ddb0c3455a51385a9a451c833cbed91b75d00a
6
+ metadata.gz: 7c38bcefaa24abe73b6d19562e3ac028aa857458ba9a60230e94b383c94b5493e1e0488ec08f4afdd6d09fd4a6cf088662be0f48e21da55cbadf45eaf1599f47
7
+ data.tar.gz: ddaf456e0d4d72f433f1d08f7dcf0c12280b228f43aaca4119ed60201170bcf57ba3cb9383be0b86e179ac664b568bb7fa37dbb5695d6f2a8583e8edc8b706c7
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
@@ -34,6 +34,10 @@ run MyApp
34
34
 
35
35
  # Prevented Attacks
36
36
 
37
+ ## DNS rebinding and other Host header attacks
38
+
39
+ * [`Rack::Protection::HostAuthorization`][host-authorization] (not included by `use Rack::Protection`)
40
+
37
41
  ## Cross Site Request Forgery
38
42
 
39
43
  Prevented by:
@@ -69,7 +73,7 @@ Prevented by:
69
73
 
70
74
  Prevented by:
71
75
 
72
- * [`Rack::Protection::SessionHijacking`][session-hijacking]
76
+ * [`Rack::Protection::SessionHijacking`][session-hijacking] (not included by `use Rack::Protection`)
73
77
 
74
78
  ## Cookie Tossing
75
79
 
@@ -109,6 +113,7 @@ The instrumenter is passed a namespace (String) and environment (Hash). The name
109
113
  [escaped-params]: http://www.sinatrarb.com/protection/escaped_params
110
114
  [form-token]: http://www.sinatrarb.com/protection/form_token
111
115
  [frame-options]: http://www.sinatrarb.com/protection/frame_options
116
+ [host-authorization]: https://github.com/sinatra/sinatra/blob/main/rack-protection/lib/rack/protection/host_authorization.rb
112
117
  [http-origin]: http://www.sinatrarb.com/protection/http_origin
113
118
  [ip-spoofing]: http://www.sinatrarb.com/protection/ip_spoofing
114
119
  [json-csrf]: http://www.sinatrarb.com/protection/json_csrf
@@ -46,14 +46,15 @@ module Rack
46
46
  # Install the gem, then run the program:
47
47
  #
48
48
  # gem install 'rack-protection'
49
- # ruby server.rb
49
+ # puma server.ru
50
50
  #
51
- # Here is <tt>server.rb</tt>:
51
+ # Here is <tt>server.ru</tt>:
52
52
  #
53
53
  # require 'rack/protection'
54
+ # require 'rack/session'
54
55
  #
55
56
  # app = Rack::Builder.app do
56
- # use Rack::Session::Cookie, secret: 'secret'
57
+ # use Rack::Session::Cookie, secret: 'CHANGEMEaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
57
58
  # use Rack::Protection::AuthenticityToken
58
59
  #
59
60
  # run -> (env) do
@@ -87,7 +88,7 @@ module Rack
87
88
  # end
88
89
  # end
89
90
  #
90
- # Rack::Handler::WEBrick.run app
91
+ # run app
91
92
  #
92
93
  # == Example: Customize which POST parameter holds the token
93
94
  #
@@ -58,6 +58,13 @@ module Rack
58
58
  result if (Array === result) && (result.size == 3)
59
59
  end
60
60
 
61
+ def debug(env, message)
62
+ return unless options[:logging]
63
+
64
+ l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
65
+ l.debug(message)
66
+ end
67
+
61
68
  def warn(env, message)
62
69
  return unless options[:logging]
63
70
 
@@ -74,7 +81,7 @@ module Rack
74
81
 
75
82
  def deny(env)
76
83
  warn env, "attack prevented by #{self.class}"
77
- [options[:status], { 'Content-Type' => 'text/plain' }, [options[:message]]]
84
+ [options[:status], { 'content-type' => 'text/plain' }, [options[:message]]]
78
85
  end
79
86
 
80
87
  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
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/protection'
4
+ require 'ipaddr'
5
+
6
+ module Rack
7
+ module Protection
8
+ ##
9
+ # Prevented attack:: DNS rebinding and other Host header attacks
10
+ # Supported browsers:: all
11
+ # More infos:: https://en.wikipedia.org/wiki/DNS_rebinding
12
+ # https://portswigger.net/web-security/host-header
13
+ #
14
+ # Blocks HTTP requests with an unrecognized hostname in any of the following
15
+ # HTTP headers: Host, X-Forwarded-Host, Forwarded
16
+ #
17
+ # If you want to permit a specific hostname, you can pass in as the `:permitted_hosts` option:
18
+ #
19
+ # use Rack::Protection::HostAuthorization, permitted_hosts: ["www.example.org", "sinatrarb.com"]
20
+ #
21
+ # The `:allow_if` option can also be set to a proc to use custom allow/deny logic.
22
+ class HostAuthorization < Base
23
+ DOT = '.'
24
+ PORT_REGEXP = /:\d+\z/.freeze
25
+ SUBDOMAINS = /[a-z0-9\-.]+/.freeze
26
+ private_constant :DOT,
27
+ :PORT_REGEXP,
28
+ :SUBDOMAINS
29
+ default_reaction :deny
30
+ default_options allow_if: nil,
31
+ message: 'Host not permitted'
32
+
33
+ def initialize(*)
34
+ super
35
+ @permitted_hosts = []
36
+ @domain_hosts = []
37
+ @ip_hosts = []
38
+ @all_permitted_hosts = Array(options[:permitted_hosts])
39
+
40
+ @all_permitted_hosts.each do |host|
41
+ case host
42
+ when String
43
+ if host.start_with?(DOT)
44
+ domain = host[1..-1]
45
+ @permitted_hosts << domain.downcase
46
+ @domain_hosts << /\A#{SUBDOMAINS}#{Regexp.escape(domain)}\z/i
47
+ else
48
+ @permitted_hosts << host.downcase
49
+ end
50
+ when IPAddr then @ip_hosts << host
51
+ end
52
+ end
53
+ end
54
+
55
+ def accepts?(env)
56
+ return true if options[:allow_if]&.call(env)
57
+ return true if @all_permitted_hosts.empty?
58
+
59
+ request = Request.new(env)
60
+ origin_host = extract_host(request.host_authority)
61
+ forwarded_host = extract_host(request.forwarded_authority)
62
+
63
+ debug env, "#{self.class} " \
64
+ "@all_permitted_hosts=#{@all_permitted_hosts.inspect} " \
65
+ "@permitted_hosts=#{@permitted_hosts.inspect} " \
66
+ "@domain_hosts=#{@domain_hosts.inspect} " \
67
+ "@ip_hosts=#{@ip_hosts.inspect} " \
68
+ "origin_host=#{origin_host.inspect} " \
69
+ "forwarded_host=#{forwarded_host.inspect}"
70
+
71
+ if host_permitted?(origin_host)
72
+ if forwarded_host.nil?
73
+ true
74
+ else
75
+ host_permitted?(forwarded_host)
76
+ end
77
+ else
78
+ false
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def extract_host(authority)
85
+ authority.to_s.split(PORT_REGEXP).first&.downcase
86
+ end
87
+
88
+ def host_permitted?(host)
89
+ exact_match?(host) || domain_match?(host) || ip_match?(host)
90
+ end
91
+
92
+ def exact_match?(host)
93
+ @permitted_hosts.include?(host)
94
+ end
95
+
96
+ def domain_match?(host)
97
+ return false if host.nil?
98
+ return false if host.start_with?(DOT)
99
+
100
+ @domain_hosts.any? { |domain_host| host.match?(domain_host) }
101
+ end
102
+
103
+ def ip_match?(host)
104
+ @ip_hosts.any? { |ip_host| ip_host.include?(host) }
105
+ rescue IPAddr::InvalidAddressError
106
+ false
107
+ end
108
+ end
109
+ end
110
+ 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.1.1'
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,11 +9,10 @@ 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'
15
+ autoload :HostAuthorization, 'rack/protection/host_authorization'
17
16
  autoload :HttpOrigin, 'rack/protection/http_origin'
18
17
  autoload :IPSpoofing, 'rack/protection/ip_spoofing'
19
18
  autoload :JsonCsrf, 'rack/protection/json_csrf'
@@ -26,12 +25,11 @@ module Rack
26
25
  autoload :XSSHeader, 'rack/protection/xss_header'
27
26
 
28
27
  def self.new(app, options = {})
29
- # does not include: RemoteReferrer, AuthenticityToken and FormToken
30
28
  except = Array options[:except]
31
29
  use_these = Array options[:use]
32
30
 
33
31
  if options.fetch(:without_session, false)
34
- except += %i[session_hijacking remote_token]
32
+ except += %i[remote_token]
35
33
  end
36
34
 
37
35
  Rack::Builder.new do
@@ -43,6 +41,7 @@ module Rack
43
41
  use ::Rack::Protection::FormToken, options if use_these.include? :form_token
44
42
  use ::Rack::Protection::ReferrerPolicy, options if use_these.include? :referrer_policy
45
43
  use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
44
+ use ::Rack::Protection::SessionHijacking, options if use_these.include? :session_hijacking
46
45
  use ::Rack::Protection::StrictTransport, options if use_these.include? :strict_transport
47
46
 
48
47
  # On by default, unless skipped
@@ -52,7 +51,6 @@ module Rack
52
51
  use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
53
52
  use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
54
53
  use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
55
- use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
56
54
  use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
57
55
  run app
58
56
  end.to_app
@@ -36,9 +36,10 @@ 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 'logger', '>= 1.6.0'
44
+ s.add_dependency 'rack', '>= 3.0.0', '< 4'
44
45
  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.1.1
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-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -25,25 +25,39 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.1.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: rack
28
+ name: logger
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: 1.6.0
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
- - - "~>"
38
+ - - ">="
42
39
  - !ruby/object:Gem::Version
43
- version: '2.2'
40
+ version: 1.6.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
44
45
  - - ">="
45
46
  - !ruby/object:Gem::Version
46
- version: 2.2.4
47
+ version: 3.0.0
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '4'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 3.0.0
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '4'
47
61
  description: Protect against typical web attacks, works with all Rack apps, including
48
62
  Rails
49
63
  email: sinatrarb@googlegroups.com
@@ -61,11 +75,10 @@ files:
61
75
  - lib/rack/protection/base.rb
62
76
  - lib/rack/protection/content_security_policy.rb
63
77
  - lib/rack/protection/cookie_tossing.rb
64
- - lib/rack/protection/encrypted_cookie.rb
65
- - lib/rack/protection/encryptor.rb
66
78
  - lib/rack/protection/escaped_params.rb
67
79
  - lib/rack/protection/form_token.rb
68
80
  - lib/rack/protection/frame_options.rb
81
+ - lib/rack/protection/host_authorization.rb
69
82
  - lib/rack/protection/http_origin.rb
70
83
  - lib/rack/protection/ip_spoofing.rb
71
84
  - lib/rack/protection/json_csrf.rb
@@ -95,14 +108,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
95
108
  requirements:
96
109
  - - ">="
97
110
  - !ruby/object:Gem::Version
98
- version: 2.6.0
111
+ version: 2.7.8
99
112
  required_rubygems_version: !ruby/object:Gem::Requirement
100
113
  requirements:
101
114
  - - ">="
102
115
  - !ruby/object:Gem::Version
103
116
  version: '0'
104
117
  requirements: []
105
- rubygems_version: 3.5.3
118
+ rubygems_version: 3.5.22
106
119
  signing_key:
107
120
  specification_version: 4
108
121
  summary: Protect against typical web attacks, works with all Rack apps, including
@@ -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