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 +4 -4
- data/Gemfile +2 -5
- data/README.md +6 -1
- data/lib/rack/protection/authenticity_token.rb +5 -4
- data/lib/rack/protection/base.rb +8 -1
- data/lib/rack/protection/content_security_policy.rb +2 -2
- data/lib/rack/protection/cookie_tossing.rb +1 -1
- data/lib/rack/protection/frame_options.rb +1 -1
- data/lib/rack/protection/host_authorization.rb +110 -0
- data/lib/rack/protection/json_csrf.rb +1 -1
- data/lib/rack/protection/referrer_policy.rb +1 -1
- data/lib/rack/protection/strict_transport.rb +1 -1
- data/lib/rack/protection/version.rb +1 -1
- data/lib/rack/protection/xss_header.rb +2 -2
- data/lib/rack/protection.rb +3 -5
- data/rack-protection.gemspec +3 -2
- metadata +27 -14
- data/lib/rack/protection/encrypted_cookie.rb +0 -273
- data/lib/rack/protection/encryptor.rb +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98553e74b3c41ed1630ad332c07ac4be4d2cf7f9f7d42c4ea383ed10b36cfda8
|
4
|
+
data.tar.gz: d1af8ebd50af6447a3879ae531de1dd5e664cb7493919f2af94c4d2a5d0036d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
#
|
49
|
+
# puma server.ru
|
50
50
|
#
|
51
|
-
# Here is <tt>server.
|
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: '
|
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
|
-
#
|
91
|
+
# run app
|
91
92
|
#
|
92
93
|
# == Example: Customize which POST parameter holds the token
|
93
94
|
#
|
data/lib/rack/protection/base.rb
CHANGED
@@ -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], { '
|
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 '
|
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] ? '
|
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, { '
|
54
|
+
[302, { 'content-type' => 'text/html', 'location' => request.path }, []]
|
55
55
|
end
|
56
56
|
|
57
57
|
def bad_cookies
|
@@ -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['
|
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
|
@@ -18,8 +18,8 @@ module Rack
|
|
18
18
|
|
19
19
|
def call(env)
|
20
20
|
status, headers, body = @app.call(env)
|
21
|
-
headers['
|
22
|
-
headers['
|
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
|
data/lib/rack/protection.rb
CHANGED
@@ -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[
|
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
|
data/rack-protection.gemspec
CHANGED
@@ -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.
|
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 '
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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.
|
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.
|
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
|