Class: Rack::Protection::AuthenticityToken
| Relationships & Source Files | |
| Extension / Inclusion / Inheritance Descendants | |
|
Subclasses:
|
|
| Super Chains via Extension / Inclusion / Inheritance | |
|
Class Chain:
self,
Base
|
|
|
Instance Chain:
self,
Base
|
|
| Inherits: |
Rack::Protection::Base
|
| Defined in: | rack-protection/lib/rack/protection/authenticity_token.rb |
Overview
- Prevented attack
CSRF
- Supported browsers
all
- More infos
This middleware only accepts requests other than GET, HEAD, OPTIONS, TRACE if their given access token matches the token included in the session.
It checks the X-CSRF-Token header and the POST form data.
It is not OOTB-compatible with the rack-csrf gem. For that, the following patch needs to be applied:
Rack::Protection::AuthenticityToken.(key: "csrf.token", authenticity_param: "_csrf")
Options
[:authenticity_param] the name of the param that should contain the token on a request. Default value: "authenticity_token"
[:key] the name of the param that should contain the token in the session. Default value: :csrf
[:allow_if] a proc for custom allow/deny logic. Will be passed env as the only arg and should return true if the request is allowed, regardless of the authenticity token. Default is nil, which means there is no special logic for allowing requests that don't have a valid token.
[Rack::Protection::Base options] any option supported by Rack::Protection::Base.
Example: Forms application
To show what the AuthenticityToken does, this section includes a sample
program which shows two forms. One with, and one without a CSRF token
The one without CSRF token field will get a 403 Forbidden response.
Install the gem, then run the program:
gem install 'rack-protection'
puma server.ru
Here is server.ru:
require 'rack/protection'
require 'rack/session'
app = Rack::Builder.app do
use Rack::Session::Cookie, secret: 'CHANGEMEaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
use Rack::Protection::AuthenticityToken
run -> (env) do
[200, {}, [
<<~EOS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>rack-protection minimal example</title>
</head>
<body>
<h1>Without Authenticity Token</h1>
<p>This takes you to <tt>Forbidden</tt></p>
<form action="" method="post">
<input type="text" name="foo" />
<input type="submit" />
</form>
<h1>With Authenticity Token</h1>
<p>This successfully takes you to back to this form.</p>
<form action="" method="post">
<input type="hidden" name="authenticity_token" value="#{Rack::Protection::AuthenticityToken.token(env['rack.session'])}" />
<input type="text" name="foo" />
<input type="submit" />
</form>
</body>
</html>
EOS
]]
end
end
run app
Example: Customize which POST parameter holds the token
To customize the authenticity parameter for form data, use the :authenticity_param option:
use Rack::Protection::AuthenticityToken, authenticity_param: 'your_token_param_name'
Constant Summary
-
GLOBAL_TOKEN_IDENTIFIER =
private
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 143'!real_csrf_token' -
TOKEN_LENGTH =
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 10332
Base - Inherited
Class Method Summary
- .random_token
-
.token(session, path: nil, method: :post)
Access the token from the given session.
Base - Inherited
| .default_options | Used by subclasses to declare default values for options they require. |
| .default_reaction | Used by subclasses to declare default reaction when a request is rejected. |
| .new | |
Instance Attribute Summary
Instance Method Summary
- #accepts?(env) ⇒ Boolean
- #mask_authenticity_token(session, path: nil, method: :post)
- #compare_with_global_token(token, session) private
- #compare_with_per_form_token(token, session, request) private
- #compare_with_real_token(token, session) private
- #decode_token(token) private
- #encode_token(token) private
- #global_token(session) private
-
#mask_token(token)
private
Creates a masked version of the authenticity token that varies on each request.
- #masked_token?(token) ⇒ Boolean private
- #per_form_token(session, path, method) private
- #real_token(session) private
- #set_token(session) private
- #token_hmac(session, identifier) private
-
#unmask_token(masked_token)
private
Essentially the inverse of #mask_token.
- #unmasked_token?(token) ⇒ Boolean private
-
#valid_token?(env, token) ⇒ Boolean
private
Checks the client's masked token to see if it matches the session token.
- #xor_byte_strings(s1, s2) private
Base - Inherited
| #accepts?, #call, #debug, #default_options, | |
| #default_reaction | Alias for Base#deny. |
| #deny | Deny the request. |
| #drop_session, #encrypt, #html?, #instrument, #origin, #random_string, #react, #referrer, | |
| #report | When used as a reaction (with option reaction: :report), any rejected request will be allowed through, and a warning is omitted (note that warnings will not be shown if the :logging option has been set to false). |
| #safe?, #secure_compare, #session, #session?, #warn | |
Constructor Details
This class inherits a constructor from Rack::Protection::Base
Class Method Details
.random_token
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 115
def self.random_token SecureRandom.urlsafe_base64(TOKEN_LENGTH, padding: false) end
.token(session, path: nil, method: :post)
Access the token from the given session. Useful for building a form that should include the current authenticity token.
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 111
def self.token(session, path: nil, method: :post) new(nil).mask_authenticity_token(session, path: path, method: method) end
Instance Method Details
#accepts?(env) ⇒ Boolean
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 119
def accepts?(env) session = session(env) set_token(session) safe?(env) || valid_token?(env, env['HTTP_X_CSRF_TOKEN']) || valid_token?(env, Request.new(env).params[[:authenticity_param]]) || [:allow_if]&.call(env) rescue StandardError false end
#compare_with_global_token(token, session) (private)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 213
def compare_with_global_token(token, session) secure_compare(token, global_token(session)) end
#compare_with_per_form_token(token, session, request) (private)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 217
def compare_with_per_form_token(token, session, request) secure_compare(token, per_form_token(session, request.path.chomp('/'), request.request_method)) end
#compare_with_real_token(token, session) (private)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 209
def compare_with_real_token(token, session) secure_compare(token, real_token(session)) end
#decode_token(token) (private)
[ GitHub ]#encode_token(token) (private)
[ GitHub ]#global_token(session) (private)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 226
def global_token(session) token_hmac(session, GLOBAL_TOKEN_IDENTIFIER) end
#mask_authenticity_token(session, path: nil, method: :post)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 131
def mask_authenticity_token(session, path: nil, method: :post) set_token(session) token = if path && method per_form_token(session, path, method) else global_token(session) end mask_token(token) end
#mask_token(token) (private)
Creates a masked version of the authenticity token that varies on each request. The masking is used to mitigate SSL attacks like BREACH.
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 184
def mask_token(token) one_time_pad = SecureRandom.random_bytes(token.length) encrypted_token = xor_byte_strings(one_time_pad, token) masked_token = one_time_pad + encrypted_token encode_token(masked_token) end
#masked_token?(token) ⇒ Boolean (private)
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 205
def masked_token?(token) token.length == TOKEN_LENGTH * 2 end
#per_form_token(session, path, method) (private)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 230
def per_form_token(session, path, method) token_hmac(session, "#{path}##{method.downcase}") end
#real_token(session) (private)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 222
def real_token(session) decode_token(session[[:key]]) end
#set_token(session) (private)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 148
def set_token(session) session[[:key]] ||= self.class.random_token end
#token_hmac(session, identifier) (private)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 242
def token_hmac(session, identifier) OpenSSL::HMAC.digest( OpenSSL::Digest.new('SHA256'), real_token(session), identifier ) end
#unmask_token(masked_token) (private)
Essentially the inverse of #mask_token.
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 192
def unmask_token(masked_token) # Split the token into the one-time pad and the encrypted # value and decrypt it token_length = masked_token.length / 2 one_time_pad = masked_token[0...token_length] encrypted_token = masked_token[token_length..] xor_byte_strings(one_time_pad, encrypted_token) end
#unmasked_token?(token) ⇒ Boolean (private)
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 201
def unmasked_token?(token) token.length == TOKEN_LENGTH end
#valid_token?(env, token) ⇒ Boolean (private)
Checks the client's masked token to see if it matches the session token.
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 154
def valid_token?(env, token) return false if token.nil? || !token.is_a?(String) || token.empty? session = session(env) begin token = decode_token(token) rescue ArgumentError # encoded_masked_token is invalid Base64 return false end # See if it's actually a masked token or not. We should be able # to handle any unmasked tokens that we've issued without error. if unmasked_token?(token) compare_with_real_token(token, session) elsif masked_token?(token) token = unmask_token(token) compare_with_global_token(token, session) || compare_with_real_token(token, session) || compare_with_per_form_token(token, session, Request.new(env)) else false # Token is malformed end end
#xor_byte_strings(s1, s2) (private)
[ GitHub ]# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 250
def xor_byte_strings(s1, s2) s2 = s2.dup size = s1.bytesize i = 0 while i < size s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i)) i += 1 end s2 end