rack-protection 3.2.0 → 4.1.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: 4c04192c3b3ef137443ac72495e4d948a544c1f7c7fd514f422045b870071b54
4
+ data.tar.gz: 5fc1d9022ef2d26e1c18dd56752883d3d99c6293fbd2a67708628bd414ab2f3f
5
5
  SHA512:
6
- metadata.gz: eeaff5e584a8ee3be6c80dc92c67fcc95bdbb97b084509ed90ca9ad524598fba63690cfa372586edd27940cd609fa44210637e9c95fbf1191e1a5cc297f222ac
7
- data.tar.gz: 26e2160d65b6015c7aaa52266b7241d15f645eb259d1371b864b3e2b6a3b1fbef841e62304bc8e39a83fab1a52ddb0c3455a51385a9a451c833cbed91b75d00a
6
+ metadata.gz: 26d822dde064625a3520d8bd95b0b2499e4d94d081e8943bfeb4283cab18fd0f70d98b7e5e3cdea5298a75edc70a0a5fc44444e5563fab90db179136901758b6
7
+ data.tar.gz: d9618b6af6ffc69006e8404a1dc18dc4bd082a516042f25d804613e67438a27830f81b3fc87f2f82d2c7ff5e6607d461e30117515bf2f0be9ea36a0ff094f28d
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.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,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.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-11-18 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