Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mitigate BREACH attack #1171

Merged
merged 1 commit into from Aug 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
103 changes: 100 additions & 3 deletions rack-protection/lib/rack/protection/authenticity_token.rb
@@ -1,4 +1,6 @@
require 'rack/protection'
require 'securerandom'
require 'base64'

module Rack
module Protection
Expand All @@ -18,17 +20,112 @@ module Protection
#
class AuthenticityToken < Base
default_options :authenticity_param => 'authenticity_token',
:authenticity_token_length => 32,
:allow_if => nil

class << self
def token(session)
mask_token(session[:csrf])
end

def random_token(length = 32)
SecureRandom.base64(length)
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 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
encode_token masked_token
end

# Essentially the inverse of +mask_token+.
def unmask_decoded_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..-1]
xor_byte_strings(one_time_pad, encrypted_token)
end

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

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

private

def xor_byte_strings(s1, s2)
s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
end
end

def accepts?(env)
session = session env
token = session[:csrf] ||= session['_csrf_token'] || random_string
session[:csrf] ||= self.class.random_token(token_length)

safe?(env) ||
secure_compare(env['HTTP_X_CSRF_TOKEN'], token) ||
secure_compare(Request.new(env).params[options[:authenticity_param]], token) ||
valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
valid_token?(session, Request.new(env).params[options[:authenticity_param]]) ||
( options[:allow_if] && options[:allow_if].call(env) )
end

private

def token_length
options[:authenticity_token_length]
end

# Checks the client's masked token to see if it matches the
# session token.
def valid_token?(session, token)
return false if token.nil? || token.empty?

begin
token = self.class.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 = self.class.unmask_decoded_token(token)

compare_with_real_token token, session

else
false # Token is malformed
end
end

def unmasked_token?(token)
token.length == token_length
end

def masked_token?(token)
token.length == token_length * 2
end

def compare_with_real_token(token, session)
secure_compare(token, real_token(session))
end

def real_token(session)
self.class.decode_token(session[:csrf])
end
end
end
end
@@ -1,27 +1,41 @@
describe Rack::Protection::AuthenticityToken do
let(:token) { described_class.random_token }
let(:bad_token) { described_class.random_token }
let(:session) { {:csrf => token} }

it_behaves_like "any rack application"

it "denies post requests without any token" do
expect(post('/')).not_to be_ok
end

it "accepts post requests with correct X-CSRF-Token header" do
post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "a")
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token)
expect(last_response).to be_ok
end

it "accepts post requests with masked X-CSRF-Token header" do
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => Rack::Protection::AuthenticityToken.token(session))
expect(last_response).to be_ok
end

it "denies post requests with wrong X-CSRF-Token header" do
post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "b")
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token)
expect(last_response).not_to be_ok
end

it "accepts post form requests with correct authenticity_token field" do
post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "a"})
post('/', {"authenticity_token" => token}, 'rack.session' => session)
expect(last_response).to be_ok
end

it "accepts post form requests with masked authenticity_token field" do
post('/', {"authenticity_token" => Rack::Protection::AuthenticityToken.token(session)}, 'rack.session' => session)
expect(last_response).to be_ok
end

it "denies post form requests with wrong authenticity_token field" do
post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "b"})
post('/', {"authenticity_token" => bad_token}, 'rack.session' => session)
expect(last_response).not_to be_ok
end

Expand All @@ -35,12 +49,39 @@
run proc { |e| [200, {'Content-Type' => 'text/plain'}, ['hi']] }
end

post('/', {"csrf_param" => "a"}, 'rack.session' => {:csrf => "a"})
post('/', {"csrf_param" => token}, 'rack.session' => {:csrf => token})
expect(last_response).to be_ok
end

it "sets a new csrf token for the session in env, even after a 'safe' request" do
get('/', {}, {})
expect(env['rack.session'][:csrf]).not_to be_nil
end

describe ".random_token" do
it "outputs a base64 encoded 32 character string by default" do
expect(Base64.strict_decode64(token).length).to eq(32)
end

it "outputs a base64 encoded string of the specified length" do
token = described_class.random_token(64)
expect(Base64.strict_decode64(token).length).to eq(64)
end
end

describe ".mask_token" do
it "generates unique masked values for a token" do
first_masked_token = described_class.mask_token(token)
second_masked_token = described_class.mask_token(token)
expect(first_masked_token).not_to eq(second_masked_token)
end
end

describe ".unmask_decoded_token" do
it "unmasks a token to its original decoded value" do
masked_token = described_class.decode_token(described_class.mask_token(token))
unmasked_token = described_class.unmask_decoded_token(masked_token)
expect(unmasked_token).to eq(described_class.decode_token(token))
end
end
end
22 changes: 18 additions & 4 deletions rack-protection/spec/lib/rack/protection/form_token_spec.rb
@@ -1,27 +1,41 @@
describe Rack::Protection::FormToken do
let(:token) { described_class.random_token }
let(:bad_token) { described_class.random_token }
let(:session) { {:csrf => token} }

it_behaves_like "any rack application"

it "denies post requests without any token" do
expect(post('/')).not_to be_ok
end

it "accepts post requests with correct X-CSRF-Token header" do
post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "a")
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token)
expect(last_response).to be_ok
end

it "accepts post requests with masked X-CSRF-Token header" do
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => Rack::Protection::FormToken.token(session))
expect(last_response).to be_ok
end

it "denies post requests with wrong X-CSRF-Token header" do
post('/', {}, 'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "b")
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token)
expect(last_response).not_to be_ok
end

it "accepts post form requests with correct authenticity_token field" do
post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "a"})
post('/', {"authenticity_token" => token}, 'rack.session' => session)
expect(last_response).to be_ok
end

it "accepts post form requests with masked authenticity_token field" do
post('/', {"authenticity_token" => Rack::Protection::FormToken.token(session)}, 'rack.session' => session)
expect(last_response).to be_ok
end

it "denies post form requests with wrong authenticity_token field" do
post('/', {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "b"})
post('/', {"authenticity_token" => bad_token}, 'rack.session' => session)
expect(last_response).not_to be_ok
end

Expand Down
28 changes: 22 additions & 6 deletions rack-protection/spec/lib/rack/protection/remote_token_spec.rb
@@ -1,4 +1,8 @@
describe Rack::Protection::RemoteToken do
let(:token) { described_class.random_token }
let(:bad_token) { described_class.random_token }
let(:session) { {:csrf => token} }

it_behaves_like "any rack application"

it "accepts post requests with no referrer" do
Expand All @@ -16,25 +20,37 @@

it "accepts post requests with a remote referrer and correct X-CSRF-Token header" do
post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org',
'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "a")
'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token)
expect(last_response).to be_ok
end

it "accepts post requests with a remote referrer and masked X-CSRF-Token header" do
post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org',
'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => Rack::Protection::RemoteToken.token(session))
expect(last_response).to be_ok
end

it "denies post requests with a remote referrer and wrong X-CSRF-Token header" do
post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org',
'rack.session' => {:csrf => "a"}, 'HTTP_X_CSRF_TOKEN' => "b")
'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token)
expect(last_response).not_to be_ok
end

it "accepts post form requests with a remote referrer and correct authenticity_token field" do
post('/', {"authenticity_token" => "a"}, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => {:csrf => "a"})
post('/', {"authenticity_token" => token}, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => session)
expect(last_response).to be_ok
end

it "accepts post form requests with a remote referrer and masked authenticity_token field" do
post('/', {"authenticity_token" => Rack::Protection::RemoteToken.token(session)}, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => session)
expect(last_response).to be_ok
end

it "denies post form requests with a remote referrer and wrong authenticity_token field" do
post('/', {"authenticity_token" => "a"}, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => {:csrf => "b"})
post('/', {"authenticity_token" => bad_token}, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => session)
expect(last_response).not_to be_ok
end
end