rack-protection 2.1.0 → 2.2.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: a3268bb2b60f8095b38658717f5e267da2e1dfbee57f487baf39a185d3cf9266
4
- data.tar.gz: fc40122b95963a81333da038536782d85a9abdc92b823eb5d3044ef3c5c807c4
3
+ metadata.gz: 5ed141bb1184b6760816e1768b5d91246d91a01ef56cb17a163f5370df604026
4
+ data.tar.gz: f3ac32c5d25123bb9f5843a47ca17dc796a9e3469a997f32b772ab72200cff58
5
5
  SHA512:
6
- metadata.gz: 7b381c903bb99d1e8cfcd00554642aafc2644f4432987be95e094a84b0f020648efb92b4a48e082cda5749045c44d14e5f08d97a1a733bf2b1e850eeaf69d67b
7
- data.tar.gz: bfb60cf9484528f0096cd68a6da55b66fa3471407a120caff83ee961b54043f3e4aca45a219273e7e48cc85ce70742ee75dab750609239fb887dfc35dfbcae59
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
@@ -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,16 +198,42 @@ 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)
@@ -34,7 +34,7 @@ module Rack
34
34
  return true if options[:allow_if] && options[:allow_if].call(env)
35
35
 
36
36
  if options.key? :origin_whitelist
37
- warn "Rack::Protection origin_whitelist option is deprecated and will be removed, " \
37
+ warn env, "Rack::Protection origin_whitelist option is deprecated and will be removed, " \
38
38
  "use permitted_origins instead.\n"
39
39
  end
40
40
 
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Protection
3
- VERSION = '2.1.0'
3
+ VERSION = '2.2.0'
4
4
  end
5
5
  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: 2.1.0
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-09-04 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