mirror of
https://github.com/sinatra/sinatra
synced 2023-03-27 23:18:01 -04:00
Merge pull request #1653 from jkowens/per-form-tokens
Add support for per form csrf tokens
This commit is contained in:
commit
ed2add3785
2 changed files with 67 additions and 17 deletions
|
@ -1,5 +1,6 @@
|
|||
require 'rack/protection'
|
||||
require 'securerandom'
|
||||
require 'openssl'
|
||||
require 'base64'
|
||||
|
||||
module Rack
|
||||
|
@ -95,29 +96,39 @@ module Rack
|
|||
:key => :csrf,
|
||||
:allow_if => nil
|
||||
|
||||
def self.token(session)
|
||||
self.new(nil).mask_authenticity_token(session)
|
||||
def self.token(session, path: nil, method: :post)
|
||||
self.new(nil).mask_authenticity_token(session, path: path, method: method)
|
||||
end
|
||||
|
||||
def self.random_token
|
||||
SecureRandom.base64(TOKEN_LENGTH)
|
||||
SecureRandom.urlsafe_base64(TOKEN_LENGTH, padding: false)
|
||||
end
|
||||
|
||||
def accepts?(env)
|
||||
session = session env
|
||||
session = session(env)
|
||||
set_token(session)
|
||||
|
||||
safe?(env) ||
|
||||
valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
|
||||
valid_token?(session, Request.new(env).params[options[:authenticity_param]]) ||
|
||||
valid_token?(env, env['HTTP_X_CSRF_TOKEN']) ||
|
||||
valid_token?(env, Request.new(env).params[options[:authenticity_param]]) ||
|
||||
( options[:allow_if] && options[:allow_if].call(env) )
|
||||
end
|
||||
|
||||
def mask_authenticity_token(session)
|
||||
token = set_token(session)
|
||||
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
|
||||
|
||||
GLOBAL_TOKEN_IDENTIFIER = '!real_csrf_token'
|
||||
private_constant :GLOBAL_TOKEN_IDENTIFIER
|
||||
|
||||
private
|
||||
|
||||
def set_token(session)
|
||||
|
@ -126,9 +137,11 @@ module Rack
|
|||
|
||||
# Checks the client's masked token to see if it matches the
|
||||
# session token.
|
||||
def valid_token?(session, token)
|
||||
def valid_token?(env, token)
|
||||
return false if token.nil? || token.empty?
|
||||
|
||||
session = session(env)
|
||||
|
||||
begin
|
||||
token = decode_token(token)
|
||||
rescue ArgumentError # encoded_masked_token is invalid Base64
|
||||
|
@ -139,13 +152,13 @@ module Rack
|
|||
# to handle any unmasked tokens that we've issued without error.
|
||||
|
||||
if unmasked_token?(token)
|
||||
compare_with_real_token token, session
|
||||
|
||||
compare_with_real_token(token, session)
|
||||
elsif masked_token?(token)
|
||||
token = unmask_token(token)
|
||||
|
||||
compare_with_real_token token, session
|
||||
|
||||
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
|
||||
|
@ -155,7 +168,6 @@ module Rack
|
|||
# on each request. The masking is used to mitigate SSL attacks
|
||||
# like BREACH.
|
||||
def mask_token(token)
|
||||
token = decode_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
|
||||
|
@ -184,16 +196,42 @@ module Rack
|
|||
secure_compare(token, real_token(session))
|
||||
end
|
||||
|
||||
def compare_with_global_token(token, session)
|
||||
secure_compare(token, global_token(session))
|
||||
end
|
||||
|
||||
def compare_with_per_form_token(token, session, request)
|
||||
secure_compare(token,
|
||||
per_form_token(session, request.path.chomp('/'), request.request_method)
|
||||
)
|
||||
end
|
||||
|
||||
def real_token(session)
|
||||
decode_token(session[options[:key]])
|
||||
end
|
||||
|
||||
def global_token(session)
|
||||
token_hmac(session, GLOBAL_TOKEN_IDENTIFIER)
|
||||
end
|
||||
|
||||
def per_form_token(session, path, method)
|
||||
token_hmac(session, "#{path}##{method.downcase}")
|
||||
end
|
||||
|
||||
def encode_token(token)
|
||||
Base64.strict_encode64(token)
|
||||
Base64.urlsafe_encode64(token)
|
||||
end
|
||||
|
||||
def decode_token(token)
|
||||
Base64.strict_decode64(token)
|
||||
Base64.urlsafe_decode64(token)
|
||||
end
|
||||
|
||||
def token_hmac(session, identifier)
|
||||
OpenSSL::HMAC.digest(
|
||||
OpenSSL::Digest::SHA256.new,
|
||||
real_token(session),
|
||||
identifier
|
||||
)
|
||||
end
|
||||
|
||||
def xor_byte_strings(s1, s2)
|
||||
|
|
|
@ -40,6 +40,18 @@ describe Rack::Protection::AuthenticityToken do
|
|||
expect(last_response).not_to be_ok
|
||||
end
|
||||
|
||||
it "accepts post form requests with a valid per form token" do
|
||||
token = Rack::Protection::AuthenticityToken.token(session, path: '/foo')
|
||||
post('/foo', {"authenticity_token" => token}, 'rack.session' => session)
|
||||
expect(last_response).to be_ok
|
||||
end
|
||||
|
||||
it "denies post form requests with an invalid per form token" do
|
||||
token = Rack::Protection::AuthenticityToken.token(session, path: '/foo')
|
||||
post('/bar', {"authenticity_token" => token}, 'rack.session' => session)
|
||||
expect(last_response).not_to be_ok
|
||||
end
|
||||
|
||||
it "prevents ajax requests without a valid token" do
|
||||
expect(post('/', {}, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest")).not_to be_ok
|
||||
end
|
||||
|
@ -86,7 +98,7 @@ describe Rack::Protection::AuthenticityToken do
|
|||
|
||||
describe ".random_token" do
|
||||
it "generates a base64 encoded 32 character string" do
|
||||
expect(Base64.strict_decode64(token).length).to eq(32)
|
||||
expect(Base64.urlsafe_decode64(token).length).to eq(32)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue