rack-protection 2.0.8.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca62cac87c00c55967df5d002e0244e1eabddcdf973ea007556b102760c66a74
4
- data.tar.gz: ee1ed6aed9320fee7d1e85fdb708bf34226cfb18815220f8f8ab0bafe87f4629
3
+ metadata.gz: 5ed141bb1184b6760816e1768b5d91246d91a01ef56cb17a163f5370df604026
4
+ data.tar.gz: f3ac32c5d25123bb9f5843a47ca17dc796a9e3469a997f32b772ab72200cff58
5
5
  SHA512:
6
- metadata.gz: c2493f8c141991bbba4697f60f3a84b361a5472d4fc8f370974eba485f2f910aaf51ab6b3a9246479a41ffc72f619d2587e0f987fdfb75536f0cb41115bc2b95
7
- data.tar.gz: ddb4160be67e1f13f16514d0a3959c39e417232cb708a84c9be99985c3502178fb718d4218a3b651c54d932582fb370628f0a5ed5815aa7ed39765cbf24c8de1
6
+ metadata.gz: 9d78cedbd03ffe9dba1c6bca4e4b3cf2b165b12e9befb1136bda35651e99b88ecd575a44e3f394aee8e0191f7080706ebc1a2462dc5504b19d41a3b7bb47e80b
7
+ data.tar.gz: 00466e987ae67ebd7b76be1099a99cba4852987b88dbbccf4f230b1d3ee8dfa40f627cc0463aec39d64788566e08cb71117a5c33688edbb30540949436245f56
@@ -1,5 +1,6 @@
1
1
  require 'rack/protection'
2
2
  require 'securerandom'
3
+ require 'openssl'
3
4
  require 'base64'
4
5
 
5
6
  module Rack
@@ -24,6 +25,13 @@ module Rack
24
25
  # the token on a request. Default value:
25
26
  # <tt>"authenticity_token"</tt>
26
27
  #
28
+ # [<tt>:key</tt>] the name of the param that should contain
29
+ # the token in the session. Default value:
30
+ # <tt>:csrf</tt>
31
+ #
32
+ # [<tt>:allow_if</tt>] a proc for custom allow/deny logic. Default value:
33
+ # <tt>nil</tt>
34
+ #
27
35
  # == Example: Forms application
28
36
  #
29
37
  # To show what the AuthenticityToken does, this section includes a sample
@@ -63,7 +71,7 @@ module Rack
63
71
  # <h1>With Authenticity Token</h1>
64
72
  # <p>This successfully takes you to back to this form.</p>
65
73
  # <form action="" method="post">
66
- # <input type="hidden" name="authenticity_token" value="#{env['rack.session'][:csrf]}" />
74
+ # <input type="hidden" name="authenticity_token" value="#{Rack::Protection::AuthenticityToken.token(env['rack.session'])}" />
67
75
  # <input type="text" name="foo" />
68
76
  # <input type="submit" />
69
77
  # </form>
@@ -85,42 +93,57 @@ module Rack
85
93
  TOKEN_LENGTH = 32
86
94
 
87
95
  default_options :authenticity_param => 'authenticity_token',
96
+ :key => :csrf,
88
97
  :allow_if => nil
89
98
 
90
- def self.token(session)
91
- self.new(nil).mask_authenticity_token(session)
99
+ def self.token(session, path: nil, method: :post)
100
+ self.new(nil).mask_authenticity_token(session, path: path, method: method)
92
101
  end
93
102
 
94
103
  def self.random_token
95
- SecureRandom.base64(TOKEN_LENGTH)
104
+ SecureRandom.urlsafe_base64(TOKEN_LENGTH, padding: false)
96
105
  end
97
106
 
98
107
  def accepts?(env)
99
- session = session env
108
+ session = session(env)
100
109
  set_token(session)
101
110
 
102
111
  safe?(env) ||
103
- valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
104
- valid_token?(session, Request.new(env).params[options[:authenticity_param]]) ||
112
+ valid_token?(env, env['HTTP_X_CSRF_TOKEN']) ||
113
+ valid_token?(env, Request.new(env).params[options[:authenticity_param]]) ||
105
114
  ( options[:allow_if] && options[:allow_if].call(env) )
115
+ rescue
116
+ false
106
117
  end
107
118
 
108
- def mask_authenticity_token(session)
109
- token = set_token(session)
119
+ def mask_authenticity_token(session, path: nil, method: :post)
120
+ set_token(session)
121
+
122
+ token = if path && method
123
+ per_form_token(session, path, method)
124
+ else
125
+ global_token(session)
126
+ end
127
+
110
128
  mask_token(token)
111
129
  end
112
130
 
131
+ GLOBAL_TOKEN_IDENTIFIER = '!real_csrf_token'
132
+ private_constant :GLOBAL_TOKEN_IDENTIFIER
133
+
113
134
  private
114
135
 
115
136
  def set_token(session)
116
- session[:csrf] ||= self.class.random_token
137
+ session[options[:key]] ||= self.class.random_token
117
138
  end
118
139
 
119
140
  # Checks the client's masked token to see if it matches the
120
141
  # session token.
121
- def valid_token?(session, token)
142
+ def valid_token?(env, token)
122
143
  return false if token.nil? || token.empty?
123
144
 
145
+ session = session(env)
146
+
124
147
  begin
125
148
  token = decode_token(token)
126
149
  rescue ArgumentError # encoded_masked_token is invalid Base64
@@ -131,13 +154,13 @@ module Rack
131
154
  # to handle any unmasked tokens that we've issued without error.
132
155
 
133
156
  if unmasked_token?(token)
134
- compare_with_real_token token, session
135
-
157
+ compare_with_real_token(token, session)
136
158
  elsif masked_token?(token)
137
159
  token = unmask_token(token)
138
160
 
139
- compare_with_real_token token, session
140
-
161
+ compare_with_global_token(token, session) ||
162
+ compare_with_real_token(token, session) ||
163
+ compare_with_per_form_token(token, session, Request.new(env))
141
164
  else
142
165
  false # Token is malformed
143
166
  end
@@ -147,7 +170,6 @@ module Rack
147
170
  # on each request. The masking is used to mitigate SSL attacks
148
171
  # like BREACH.
149
172
  def mask_token(token)
150
- token = decode_token(token)
151
173
  one_time_pad = SecureRandom.random_bytes(token.length)
152
174
  encrypted_token = xor_byte_strings(one_time_pad, token)
153
175
  masked_token = one_time_pad + encrypted_token
@@ -176,20 +198,53 @@ module Rack
176
198
  secure_compare(token, real_token(session))
177
199
  end
178
200
 
201
+ def compare_with_global_token(token, session)
202
+ secure_compare(token, global_token(session))
203
+ end
204
+
205
+ def compare_with_per_form_token(token, session, request)
206
+ secure_compare(token,
207
+ per_form_token(session, request.path.chomp('/'), request.request_method)
208
+ )
209
+ end
210
+
179
211
  def real_token(session)
180
- decode_token(session[:csrf])
212
+ decode_token(session[options[:key]])
213
+ end
214
+
215
+ def global_token(session)
216
+ token_hmac(session, GLOBAL_TOKEN_IDENTIFIER)
217
+ end
218
+
219
+ def per_form_token(session, path, method)
220
+ token_hmac(session, "#{path}##{method.downcase}")
181
221
  end
182
222
 
183
223
  def encode_token(token)
184
- Base64.strict_encode64(token)
224
+ Base64.urlsafe_encode64(token)
185
225
  end
186
226
 
187
227
  def decode_token(token)
188
- Base64.strict_decode64(token)
228
+ Base64.urlsafe_decode64(token)
229
+ end
230
+
231
+ def token_hmac(session, identifier)
232
+ OpenSSL::HMAC.digest(
233
+ OpenSSL::Digest::SHA256.new,
234
+ real_token(session),
235
+ identifier
236
+ )
189
237
  end
190
238
 
191
239
  def xor_byte_strings(s1, s2)
192
- s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
240
+ s2 = s2.dup
241
+ size = s1.bytesize
242
+ i = 0
243
+ while i < size
244
+ s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
245
+ i += 1
246
+ end
247
+ s2
193
248
  end
194
249
  end
195
250
  end
@@ -36,16 +36,15 @@ module Rack
36
36
  # to be used in a policy.
37
37
  #
38
38
  class ContentSecurityPolicy < Base
39
- default_options default_src: :none, script_src: "'self'",
40
- img_src: "'self'", style_src: "'self'",
41
- connect_src: "'self'", report_only: false
39
+ default_options default_src: "'self'", report_only: false
42
40
 
43
41
  DIRECTIVES = %i(base_uri child_src connect_src default_src
44
42
  font_src form_action frame_ancestors frame_src
45
43
  img_src manifest_src media_src object_src
46
44
  plugin_types referrer reflected_xss report_to
47
45
  report_uri require_sri_for sandbox script_src
48
- style_src worker_src).freeze
46
+ style_src worker_src webrtc_src navigate_to
47
+ prefetch_src).freeze
49
48
 
50
49
  NO_ARG_DIRECTIVES = %i(block_all_mixed_content disown_opener
51
50
  upgrade_insecure_requests).freeze
@@ -62,7 +61,7 @@ module Rack
62
61
  # Set these key values to boolean 'true' to include in policy
63
62
  NO_ARG_DIRECTIVES.each do |d|
64
63
  if options.key?(d) && options[d].is_a?(TrueClass)
65
- directives << d.to_s.sub(/_/, '-')
64
+ directives << d.to_s.tr('_', '-')
66
65
  end
67
66
  end
68
67
 
@@ -9,11 +9,11 @@ module Rack
9
9
  # http://tools.ietf.org/html/draft-abarth-origin
10
10
  #
11
11
  # Does not accept unsafe HTTP requests when value of Origin HTTP request header
12
- # does not match default or whitelisted URIs.
12
+ # does not match default or permitted URIs.
13
13
  #
14
- # If you want to whitelist a specific domain, you can pass in as the `:origin_whitelist` option:
14
+ # If you want to permit a specific domain, you can pass in as the `:permitted_origins` option:
15
15
  #
16
- # use Rack::Protection, origin_whitelist: ["http://localhost:3000", "http://127.0.01:3000"]
16
+ # use Rack::Protection, permitted_origins: ["http://localhost:3000", "http://127.0.01:3000"]
17
17
  #
18
18
  # The `:allow_if` option can also be set to a proc to use custom allow/deny logic.
19
19
  class HttpOrigin < Base
@@ -32,7 +32,14 @@ module Rack
32
32
  return true unless origin = env['HTTP_ORIGIN']
33
33
  return true if base_url(env) == origin
34
34
  return true if options[:allow_if] && options[:allow_if].call(env)
35
- Array(options[:origin_whitelist]).include? origin
35
+
36
+ if options.key? :origin_whitelist
37
+ warn env, "Rack::Protection origin_whitelist option is deprecated and will be removed, " \
38
+ "use permitted_origins instead.\n"
39
+ end
40
+
41
+ permitted_origins = options[:permitted_origins] || options[:origin_whitelist]
42
+ Array(permitted_origins).include? origin
36
43
  end
37
44
 
38
45
  end
@@ -0,0 +1,25 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: Secret leakage, third party tracking
7
+ # Supported browsers:: mixed support
8
+ # More infos:: https://www.w3.org/TR/referrer-policy/
9
+ # https://caniuse.com/#search=referrer-policy
10
+ #
11
+ # Sets Referrer-Policy header to tell the browser to limit the Referer header.
12
+ #
13
+ # Options:
14
+ # referrer_policy:: The policy to use (default: 'strict-origin-when-cross-origin')
15
+ class ReferrerPolicy < Base
16
+ default_options :referrer_policy => 'strict-origin-when-cross-origin'
17
+
18
+ def call(env)
19
+ status, headers, body = @app.call(env)
20
+ headers['Referrer-Policy'] ||= options[:referrer_policy]
21
+ [status, headers, body]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Protection
3
- VERSION = '2.0.8.1'
3
+ VERSION = '2.2.0'
4
4
  end
5
5
  end
@@ -14,6 +14,7 @@ module Rack
14
14
  autoload :IPSpoofing, 'rack/protection/ip_spoofing'
15
15
  autoload :JsonCsrf, 'rack/protection/json_csrf'
16
16
  autoload :PathTraversal, 'rack/protection/path_traversal'
17
+ autoload :ReferrerPolicy, 'rack/protection/referrer_policy'
17
18
  autoload :RemoteReferrer, 'rack/protection/remote_referrer'
18
19
  autoload :RemoteToken, 'rack/protection/remote_token'
19
20
  autoload :SessionHijacking, 'rack/protection/session_hijacking'
@@ -32,9 +33,11 @@ module Rack
32
33
  Rack::Builder.new do
33
34
  # Off by default, unless added
34
35
  use ::Rack::Protection::AuthenticityToken, options if use_these.include? :authenticity_token
35
- use ::Rack::Protection::CookieTossing, options if use_these.include? :cookie_tossing
36
36
  use ::Rack::Protection::ContentSecurityPolicy, options if use_these.include? :content_security_policy
37
+ use ::Rack::Protection::CookieTossing, options if use_these.include? :cookie_tossing
38
+ use ::Rack::Protection::EscapedParams, options if use_these.include? :escaped_params
37
39
  use ::Rack::Protection::FormToken, options if use_these.include? :form_token
40
+ use ::Rack::Protection::ReferrerPolicy, options if use_these.include? :referrer_policy
38
41
  use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
39
42
  use ::Rack::Protection::StrictTransport, options if use_these.include? :strict_transport
40
43
 
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: 2.0.8.1
4
+ version: 2.2.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: 2020-01-01 00:00:00.000000000 Z
11
+ date: 2022-02-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -76,6 +76,7 @@ files:
76
76
  - lib/rack/protection/ip_spoofing.rb
77
77
  - lib/rack/protection/json_csrf.rb
78
78
  - lib/rack/protection/path_traversal.rb
79
+ - lib/rack/protection/referrer_policy.rb
79
80
  - lib/rack/protection/remote_referrer.rb
80
81
  - lib/rack/protection/remote_token.rb
81
82
  - lib/rack/protection/session_hijacking.rb
@@ -105,8 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
106
  - !ruby/object:Gem::Version
106
107
  version: '0'
107
108
  requirements: []
108
- rubyforge_project:
109
- rubygems_version: 2.7.3
109
+ rubygems_version: 3.1.2
110
110
  signing_key:
111
111
  specification_version: 4
112
112
  summary: Protect against typical web attacks, works with all Rack apps, including