123456789_123456789_123456789_123456789_123456789_

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

http://en.wikipedia.org/wiki/Cross-site_request_forgery

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.default_options(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

Base - Inherited

DEFAULT_OPTIONS

Class Method Summary

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

Base - Inherited

Instance Method Summary

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.

[ GitHub ]

  
# 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

[ GitHub ]

  
# 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[options[:authenticity_param]]) ||
    options[: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 ]

  
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 238

def decode_token(token)
  Base64.urlsafe_decode64(token)
end

#encode_token(token) (private)

[ GitHub ]

  
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 234

def encode_token(token)
  Base64.urlsafe_encode64(token)
end

#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.

[ GitHub ]

  
# 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)

[ GitHub ]

  
# 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[options[:key]])
end

#set_token(session) (private)

[ GitHub ]

  
# File 'rack-protection/lib/rack/protection/authenticity_token.rb', line 148

def set_token(session)
  session[options[: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.

[ GitHub ]

  
# 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)

[ GitHub ]

  
# 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.

[ GitHub ]

  
# 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