mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Auth token mask from breach-mitigation-rails gem
This merges in the code from the breach-mitigation-rails gem that masks authenticity tokens on each request by XORing them with a random set of bytes. The masking is used to make it impossible for an attacker to steal a CSRF token from an SSL session by using techniques like the BREACH attack. The patch is pretty simple - I've copied over the [relevant code](https://github.com/meldium/breach-mitigation-rails/blob/master/lib/breach_mitigation/masking_secrets.rb) and updated the tests to pass, mostly by adjusting stubs and mocks.
This commit is contained in:
parent
4751a8c51f
commit
69fc0e1b5e
2 changed files with 71 additions and 8 deletions
|
@ -240,6 +240,8 @@ module ActionController #:nodoc:
|
|||
content_type =~ %r(\Atext/javascript) && !request.xhr?
|
||||
end
|
||||
|
||||
AUTHENTICITY_TOKEN_LENGTH = 32
|
||||
|
||||
# Returns true or false if a request is verified. Checks:
|
||||
#
|
||||
# * is it a GET or HEAD request? Gets should be safe and idempotent
|
||||
|
@ -247,13 +249,73 @@ module ActionController #:nodoc:
|
|||
# * Does the X-CSRF-Token header match the form_authenticity_token
|
||||
def verified_request?
|
||||
!protect_against_forgery? || request.get? || request.head? ||
|
||||
form_authenticity_token == form_authenticity_param ||
|
||||
form_authenticity_token == request.headers['X-CSRF-Token']
|
||||
valid_authenticity_token?(session, form_authenticity_param) ||
|
||||
valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
|
||||
end
|
||||
|
||||
# Sets the token value for the current session.
|
||||
def form_authenticity_token
|
||||
session[:_csrf_token] ||= SecureRandom.base64(32)
|
||||
masked_authenticity_token(session)
|
||||
end
|
||||
|
||||
# Creates a masked version of the authenticity token that varies
|
||||
# on each request. The masking is used to mitigate SSL attacks
|
||||
# like BREACH.
|
||||
def masked_authenticity_token(session)
|
||||
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
|
||||
encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
|
||||
masked_token = one_time_pad + encrypted_csrf_token
|
||||
Base64.strict_encode64(masked_token)
|
||||
end
|
||||
|
||||
# Checks the client's masked token to see if it matches the
|
||||
# session token. Essentially the inverse of
|
||||
# +masked_authenticity_token+.
|
||||
def valid_authenticity_token?(session, encoded_masked_token)
|
||||
return false if encoded_masked_token.nil? || encoded_masked_token.empty?
|
||||
|
||||
begin
|
||||
masked_token = Base64.strict_decode64(encoded_masked_token)
|
||||
rescue ArgumentError # encoded_masked_token is invalid Base64
|
||||
return false
|
||||
end
|
||||
|
||||
# See if it's actually a masked token or not. In order to
|
||||
# deploy this code, we should be able to handle any unmasked
|
||||
# tokens that we've issued without error.
|
||||
|
||||
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
|
||||
# This is actually an unmasked token. This is expected if
|
||||
# you have just upgraded to masked tokens, but should stop
|
||||
# happening shortly after installing this gem
|
||||
compare_with_real_token masked_token, session
|
||||
|
||||
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
|
||||
# Split the token into the one-time pad and the encrypted
|
||||
# value and decrypt it
|
||||
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
|
||||
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
|
||||
csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
|
||||
|
||||
compare_with_real_token csrf_token, session
|
||||
|
||||
else
|
||||
false # Token is malformed
|
||||
end
|
||||
end
|
||||
|
||||
def compare_with_real_token(token, session)
|
||||
# Borrow a constant-time comparison from Rack
|
||||
Rack::Utils.secure_compare(token, real_csrf_token(session))
|
||||
end
|
||||
|
||||
def real_csrf_token(session)
|
||||
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
|
||||
Base64.strict_decode64(session[:_csrf_token])
|
||||
end
|
||||
|
||||
def xor_byte_strings(s1, s2)
|
||||
s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
|
||||
end
|
||||
|
||||
# The form's authenticity parameter. Override to provide your own.
|
||||
|
|
|
@ -125,8 +125,9 @@ end
|
|||
module RequestForgeryProtectionTests
|
||||
def setup
|
||||
@token = "cf50faa3fe97702ca1ae"
|
||||
|
||||
SecureRandom.stubs(:base64).returns(@token)
|
||||
@controller.stubs(:form_authenticity_token).returns(@token)
|
||||
@controller.stubs(:valid_authenticity_token?).with{ |_, t| t == @token }.returns(true)
|
||||
@controller.stubs(:valid_authenticity_token?).with{ |_, t| t != @token }.returns(false)
|
||||
@old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
|
||||
ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
|
||||
end
|
||||
|
@ -386,7 +387,7 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController
|
|||
end
|
||||
|
||||
test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
|
||||
SecureRandom.stubs(:base64).returns(@token + '<=?')
|
||||
@controller.stubs(:form_authenticity_token).returns(@token + '<=?')
|
||||
get :meta
|
||||
assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
|
||||
assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae<=?'
|
||||
|
@ -466,7 +467,7 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase
|
|||
super
|
||||
@old_logger = ActionController::Base.logger
|
||||
@logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
|
||||
@token = "foobar"
|
||||
@token = Base64.strict_encode64(SecureRandom.random_bytes(32))
|
||||
@old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
|
||||
ActionController::Base.request_forgery_protection_token = @token
|
||||
end
|
||||
|
@ -478,7 +479,7 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase
|
|||
|
||||
def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
|
||||
ActionController::Base.logger = @logger
|
||||
SecureRandom.stubs(:base64).returns(@token)
|
||||
@controller.stubs(:valid_authenticity_token?).returns(:true)
|
||||
|
||||
begin
|
||||
post :index, :custom_token_name => 'foobar'
|
||||
|
|
Loading…
Reference in a new issue