1
0
Fork 0
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:
Bradley Buda 2014-08-19 14:29:26 -07:00
parent 4751a8c51f
commit 69fc0e1b5e
2 changed files with 71 additions and 8 deletions

View file

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

View file

@ -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&lt;=?'
@ -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'