Compare commits

..

7 Commits

Author SHA1 Message Date
Charlie Somerville
685cb901fc Merge pull request #13 from github/backport-message_verifier
Backport ActiveSupport::MessageVerifier from Rails 3
2013-08-27 03:55:52 -07:00
Charlie Somerville
e9f9d05a94 pass digest as a key in an options hash 2013-08-27 20:51:18 +10:00
Charlie Somerville
7b6670cc08 fix test 2013-08-27 20:51:14 +10:00
Charlie Somerville
ed2d852bdc backport ActiveSupport::MessageVerifier from Rails 3 2013-08-27 20:27:31 +10:00
Greg Ose
726ab5316d Merge pull request #12 from github/authenticity_token_tests
Authenticity token tests
2013-08-13 11:37:09 -07:00
Greg Ose
ecd6fb250a Test for form_for authenticity_token backport 2013-08-12 13:21:03 -05:00
Greg Ose
9f8ee9dd97 Merge tag 'github24' into authenticity_token_tests
github24
2013-08-12 12:54:23 -05:00
5 changed files with 100 additions and 49 deletions

View File

@@ -209,7 +209,7 @@ module ActionController
def verifier_for(secret, digest) def verifier_for(secret, digest)
key = secret.respond_to?(:call) ? secret.call : secret key = secret.respond_to?(:call) ? secret.call : secret
ActiveSupport::MessageVerifier.new(key, digest) ActiveSupport::MessageVerifier.new(key, digest: digest)
end end
def generate_sid def generate_sid

View File

@@ -19,6 +19,14 @@ module RequestForgeryProtectionActions
render :inline => "<% form_remote_tag(:url => '/') {} %>" render :inline => "<% form_remote_tag(:url => '/') {} %>"
end end
def external_form_for
render :inline => "<%= form_for(:some_resource, :authenticity_token => 'external_token') {} %>"
end
def form_for_without_token
render :inline => "<%= form_for(:some_resource, :authenticity_token => false ) {} %>"
end
def unsafe def unsafe
render :text => 'pwn' render :text => 'pwn'
end end
@@ -156,6 +164,20 @@ module RequestForgeryProtectionTests
assert_nothing_raised { yield } assert_nothing_raised { yield }
assert_response :success assert_response :success
end end
def test_should_render_form_with_token_tag_with_authenticity_token_requested
assert_not_blocked do
get :external_form_for
end
assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', 'external_token'
end
def test_should_render_form_without_token_tag_with_authenticity_token_set_to_false
assert_not_blocked do
get :form_for_without_token
end
assert_select 'form>div>input[name=?]', 'authenticity_token', false
end
end end
# OK let's get our test on # OK let's get our test on

View File

@@ -7,7 +7,7 @@ class CookieStoreTest < ActionController::IntegrationTest
DispatcherApp = ActionController::Dispatcher.new DispatcherApp = ActionController::Dispatcher.new
Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, 'SHA1') Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, digest: 'SHA1')
SignedBar = "BAh7BjoIZm9vIghiYXI%3D--fef868465920f415f2c0652d6910d3af288a0367" SignedBar = "BAh7BjoIZm9vIghiYXI%3D--fef868465920f415f2c0652d6910d3af288a0367"

View File

@@ -1,79 +1,74 @@
require 'active_support/base64'
require 'active_support/deprecation'
require 'active_support/core_ext/object/blank'
module ActiveSupport module ActiveSupport
# MessageVerifier makes it easy to generate and verify messages which are signed # +MessageVerifier+ makes it easy to generate and verify messages which are signed
# to prevent tampering. # to prevent tampering.
# #
# This is useful for cases like remember-me tokens and auto-unsubscribe links where the # This is useful for cases like remember-me tokens and auto-unsubscribe links where the
# session store isn't suitable or available. # session store isn't suitable or available.
# #
# Remember Me: # Remember Me:
# cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now]) # cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])
# #
# In the authentication filter: # In the authentication filter:
# #
# id, time = @verifier.verify(cookies[:remember_me]) # id, time = @verifier.verify(cookies[:remember_me])
# if time < Time.now # if time < Time.now
# self.current_user = User.find(id) # self.current_user = User.find(id)
# end # end
# #
# By default it uses Marshal to serialize the message. If you want to use another
# serialization method, you can set the serializer attribute to something that responds
# to dump and load, e.g.:
#
# @verifier.serializer = YAML
class MessageVerifier class MessageVerifier
class InvalidSignature < StandardError; end class InvalidSignature < StandardError; end
def initialize(secret, digest = 'SHA1') def initialize(secret, options = {})
unless options.is_a?(Hash)
ActiveSupport::Deprecation.warn "The second parameter should be an options hash. Use :digest => 'algorithm' to specify the digest algorithm."
options = { :digest => options }
end
@secret = secret @secret = secret
@digest = digest @digest = options[:digest] || 'SHA1'
@serializer = options[:serializer] || Marshal
end end
def verify(signed_message) def verify(signed_message)
raise InvalidSignature if signed_message.blank? raise InvalidSignature if signed_message.blank?
data, digest = signed_message.split("--") data, digest = signed_message.split("--")
if data.present? && digest.present? && secure_compare(digest, generate_digest(data)) if data.present? && digest.present? && secure_compare(digest, generate_digest(data))
Marshal.load(ActiveSupport::Base64.decode64(data)) @serializer.load(::Base64.decode64(data))
else else
raise InvalidSignature raise InvalidSignature
end end
end end
def generate(value) def generate(value)
data = ActiveSupport::Base64.encode64s(Marshal.dump(value)) data = ::Base64.strict_encode64(@serializer.dump(value))
"#{data}--#{generate_digest(data)}" "#{data}--#{generate_digest(data)}"
end end
private
if "foo".respond_to?(:force_encoding)
# constant-time comparison algorithm to prevent timing attacks
def secure_compare(a, b)
a = a.dup.force_encoding(Encoding::BINARY)
b = b.dup.force_encoding(Encoding::BINARY)
if a.length == b.length private
result = 0 # constant-time comparison algorithm to prevent timing attacks
for i in 0..(a.length - 1) def secure_compare(a, b)
result |= a[i].ord ^ b[i].ord return false unless a.bytesize == b.bytesize
end
result == 0 l = a.unpack "C#{a.bytesize}"
else
false res = 0
end b.each_byte { |byte| res |= byte ^ l.shift }
end res == 0
else
# For <= 1.8.6
def secure_compare(a, b)
if a.length == b.length
result = 0
for i in 0..(a.length - 1)
result |= a[i] ^ b[i]
end
result == 0
else
false
end
end
end end
def generate_digest(data) def generate_digest(data)
require 'openssl' unless defined?(OpenSSL) require 'openssl' unless defined?(OpenSSL)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), @secret, data) OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
end end
end end
end end

View File

@@ -1,16 +1,36 @@
require 'abstract_unit' require 'abstract_unit'
class MessageVerifierTest < Test::Unit::TestCase begin
def setup require 'openssl'
@verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") OpenSSL::Digest::SHA1
@data = {:some=>"data", :now=>Time.now} rescue LoadError, NameError
$stderr.puts "Skipping MessageVerifier test: broken OpenSSL install"
else
require 'active_support/json'
class MessageVerifierTest < ActiveSupport::TestCase
class JSONSerializer
def dump(value)
ActiveSupport::JSON.encode(value)
end
def load(value)
ActiveSupport::JSON.decode(value)
end
end end
def setup
@verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!")
@data = { :some => "data", :now => Time.local(2010) }
end
def test_simple_round_tripping def test_simple_round_tripping
message = @verifier.generate(@data) message = @verifier.generate(@data)
assert_equal @data, @verifier.verify(message) assert_equal @data, @verifier.verify(message)
end end
def test_missing_signature_raises def test_missing_signature_raises
assert_not_verified(nil) assert_not_verified(nil)
assert_not_verified("") assert_not_verified("")
@@ -23,9 +43,23 @@ class MessageVerifierTest < Test::Unit::TestCase
assert_not_verified("purejunk") assert_not_verified("purejunk")
end end
def test_alternative_serialization_method
verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", :serializer => JSONSerializer.new)
message = verifier.generate({ :foo => 123, 'bar' => Time.utc(2010) })
assert_equal verifier.verify(message), { "foo" => 123, "bar" => "2010/01/01 00:00:00 +0000" }
end
def test_digest_algorithm_as_second_parameter_deprecation
assert_deprecated(/options hash/) do
ActiveSupport::MessageVerifier.new("secret", "SHA1")
end
end
def assert_not_verified(message) def assert_not_verified(message)
assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
@verifier.verify(message) @verifier.verify(message)
end end
end end
end end
end