mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #8112 from rails/encrypted_cookies
Encrypted cookies
This commit is contained in:
commit
ef8b845de7
27 changed files with 321 additions and 92 deletions
|
@ -249,9 +249,9 @@ module ActionController
|
|||
end
|
||||
|
||||
def secret_token(request)
|
||||
secret = request.env["action_dispatch.secret_token"]
|
||||
raise "You must set config.secret_token in your app's config" if secret.blank?
|
||||
secret
|
||||
key_generator = request.env["action_dispatch.key_generator"]
|
||||
http_auth_salt = request.env["action_dispatch.http_auth_salt"]
|
||||
key_generator.generate_key(http_auth_salt)
|
||||
end
|
||||
|
||||
# Uses an MD5 digest based on time to generate a value to be used only once.
|
||||
|
|
|
@ -121,11 +121,11 @@ module ActionController #:nodoc:
|
|||
|
||||
class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
|
||||
def self.build(request)
|
||||
secret = request.env[ActionDispatch::Cookies::TOKEN_KEY]
|
||||
host = request.host
|
||||
secure = request.ssl?
|
||||
key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
|
||||
host = request.host
|
||||
secure = request.ssl?
|
||||
|
||||
new(secret, host, secure)
|
||||
new(key_generator, host, secure)
|
||||
end
|
||||
|
||||
def write(*)
|
||||
|
|
|
@ -81,10 +81,11 @@ module ActionDispatch
|
|||
end
|
||||
|
||||
module Session
|
||||
autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
|
||||
autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
|
||||
autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
|
||||
autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
|
||||
autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
|
||||
autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
|
||||
autoload :EncryptedCookieStore, 'action_dispatch/middleware/session/cookie_store'
|
||||
autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
|
||||
autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
|
||||
end
|
||||
|
||||
mattr_accessor :test_app
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
require 'active_support/core_ext/hash/keys'
|
||||
require 'active_support/core_ext/module/attribute_accessors'
|
||||
require 'active_support/message_verifier'
|
||||
|
||||
module ActionDispatch
|
||||
class Request < Rack::Request
|
||||
|
@ -27,7 +28,7 @@ module ActionDispatch
|
|||
# cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
|
||||
#
|
||||
# # Sets a signed cookie, which prevents users from tampering with its value.
|
||||
# # The cookie is signed by your app's <tt>config.secret_token</tt> value.
|
||||
# # The cookie is signed by your app's <tt>config.secret_key_base</tt> value.
|
||||
# # It can be read using the signed method <tt>cookies.signed[:key]</tt>
|
||||
# cookies.signed[:user_id] = current_user.id
|
||||
#
|
||||
|
@ -79,8 +80,12 @@ module ActionDispatch
|
|||
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
|
||||
# only HTTP. Defaults to +false+.
|
||||
class Cookies
|
||||
HTTP_HEADER = "Set-Cookie".freeze
|
||||
TOKEN_KEY = "action_dispatch.secret_token".freeze
|
||||
HTTP_HEADER = "Set-Cookie".freeze
|
||||
GENERATOR_KEY = "action_dispatch.key_generator".freeze
|
||||
SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
|
||||
ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
|
||||
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
|
||||
|
||||
|
||||
# Raised when storing more than 4K of session data.
|
||||
CookieOverflow = Class.new StandardError
|
||||
|
@ -103,21 +108,27 @@ module ActionDispatch
|
|||
DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
|
||||
|
||||
def self.build(request)
|
||||
secret = request.env[TOKEN_KEY]
|
||||
env = request.env
|
||||
key_generator = env[GENERATOR_KEY]
|
||||
options = { signed_cookie_salt: env[SIGNED_COOKIE_SALT],
|
||||
encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT],
|
||||
encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] }
|
||||
|
||||
host = request.host
|
||||
secure = request.ssl?
|
||||
|
||||
new(secret, host, secure).tap do |hash|
|
||||
new(key_generator, host, secure, options).tap do |hash|
|
||||
hash.update(request.cookies)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(secret = nil, host = nil, secure = false)
|
||||
@secret = secret
|
||||
def initialize(key_generator, host = nil, secure = false, options = {})
|
||||
@key_generator = key_generator
|
||||
@set_cookies = {}
|
||||
@delete_cookies = {}
|
||||
@host = host
|
||||
@secure = secure
|
||||
@options = options
|
||||
@cookies = {}
|
||||
end
|
||||
|
||||
|
@ -220,7 +231,7 @@ module ActionDispatch
|
|||
# cookies.permanent.signed[:remember_me] = current_user.id
|
||||
# # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
|
||||
def permanent
|
||||
@permanent ||= PermanentCookieJar.new(self, @secret)
|
||||
@permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
|
||||
end
|
||||
|
||||
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
|
||||
|
@ -228,7 +239,7 @@ module ActionDispatch
|
|||
# cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
|
||||
# be raised.
|
||||
#
|
||||
# This jar requires that you set a suitable secret for the verification on your app's +config.secret_token+.
|
||||
# This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
|
@ -237,7 +248,23 @@ module ActionDispatch
|
|||
#
|
||||
# cookies.signed[:discount] # => 45
|
||||
def signed
|
||||
@signed ||= SignedCookieJar.new(self, @secret)
|
||||
@signed ||= SignedCookieJar.new(self, @key_generator, @options)
|
||||
end
|
||||
|
||||
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
|
||||
# If the cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception
|
||||
# will be raised.
|
||||
#
|
||||
# This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# cookies.encrypted[:discount] = 45
|
||||
# # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
|
||||
#
|
||||
# cookies.encrypted[:discount] # => 45
|
||||
def encrypted
|
||||
@encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options)
|
||||
end
|
||||
|
||||
def write(headers)
|
||||
|
@ -261,8 +288,10 @@ module ActionDispatch
|
|||
end
|
||||
|
||||
class PermanentCookieJar < CookieJar #:nodoc:
|
||||
def initialize(parent_jar, secret)
|
||||
@parent_jar, @secret = parent_jar, secret
|
||||
def initialize(parent_jar, key_generator, options = {})
|
||||
@parent_jar = parent_jar
|
||||
@key_generator = key_generator
|
||||
@options = options
|
||||
end
|
||||
|
||||
def []=(key, options)
|
||||
|
@ -283,11 +312,11 @@ module ActionDispatch
|
|||
|
||||
class SignedCookieJar < CookieJar #:nodoc:
|
||||
MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes.
|
||||
SECRET_MIN_LENGTH = 30 # Characters
|
||||
|
||||
def initialize(parent_jar, secret)
|
||||
ensure_secret_secure(secret)
|
||||
def initialize(parent_jar, key_generator, options = {})
|
||||
@parent_jar = parent_jar
|
||||
@options = options
|
||||
secret = key_generator.generate_key(@options[:signed_cookie_salt])
|
||||
@verifier = ActiveSupport::MessageVerifier.new(secret)
|
||||
end
|
||||
|
||||
|
@ -314,26 +343,41 @@ module ActionDispatch
|
|||
def method_missing(method, *arguments, &block)
|
||||
@parent_jar.send(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# To prevent users from using something insecure like "Password" we make sure that the
|
||||
# secret they've provided is at least 30 characters in length.
|
||||
def ensure_secret_secure(secret)
|
||||
if secret.blank?
|
||||
raise ArgumentError, "A secret is required to generate an " +
|
||||
"integrity hash for cookie session data. Use " +
|
||||
"config.secret_token = \"some secret phrase of at " +
|
||||
"least #{SECRET_MIN_LENGTH} characters\"" +
|
||||
"in config/initializers/secret_token.rb"
|
||||
class EncryptedCookieJar < SignedCookieJar #:nodoc:
|
||||
def initialize(parent_jar, key_generator, options = {})
|
||||
if ActiveSupport::DummyKeyGenerator === key_generator
|
||||
raise "Encrypted Cookies must be used in conjunction with config.secret_key_base." +
|
||||
"Set config.secret_key_base in config/initializers/secret_token.rb"
|
||||
end
|
||||
|
||||
if secret.length < SECRET_MIN_LENGTH
|
||||
raise ArgumentError, "Secret should be something secure, " +
|
||||
"like \"#{SecureRandom.hex(16)}\". The value you " +
|
||||
"provided, \"#{secret}\", is shorter than the minimum length " +
|
||||
"of #{SECRET_MIN_LENGTH} characters"
|
||||
@parent_jar = parent_jar
|
||||
@options = options
|
||||
secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
|
||||
sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
|
||||
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
|
||||
end
|
||||
|
||||
def [](name)
|
||||
if encrypted_message = @parent_jar[name]
|
||||
@encryptor.decrypt_and_verify(encrypted_message)
|
||||
end
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature,
|
||||
ActiveSupport::MessageVerifier::InvalidMessage
|
||||
nil
|
||||
end
|
||||
|
||||
def []=(key, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
else
|
||||
options = { :value => options }
|
||||
end
|
||||
options[:value] = @encryptor.encrypt_and_sign(options[:value])
|
||||
|
||||
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
|
||||
@parent_jar[key] = options
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -57,8 +57,7 @@ module ActionDispatch
|
|||
def unpacked_cookie_data(env)
|
||||
env["action_dispatch.request.unsigned_session_cookie"] ||= begin
|
||||
stale_session_check! do
|
||||
request = ActionDispatch::Request.new(env)
|
||||
if data = request.cookie_jar.signed[@key]
|
||||
if data = cookie_jar(env)[@key]
|
||||
data.stringify_keys!
|
||||
end
|
||||
data || {}
|
||||
|
@ -72,8 +71,26 @@ module ActionDispatch
|
|||
end
|
||||
|
||||
def set_cookie(env, session_id, cookie)
|
||||
cookie_jar(env)[@key] = cookie
|
||||
end
|
||||
|
||||
def get_cookie
|
||||
cookie_jar(env)[@key]
|
||||
end
|
||||
|
||||
def cookie_jar(env)
|
||||
request = ActionDispatch::Request.new(env)
|
||||
request.cookie_jar.signed[@key] = cookie
|
||||
request.cookie_jar.signed
|
||||
end
|
||||
end
|
||||
|
||||
class EncryptedCookieStore < CookieStore
|
||||
|
||||
private
|
||||
|
||||
def cookie_jar(env)
|
||||
request = ActionDispatch::Request.new(env)
|
||||
request.cookie_jar.encrypted
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,10 @@ module ActionDispatch
|
|||
config.action_dispatch.rescue_responses = { }
|
||||
config.action_dispatch.default_charset = nil
|
||||
config.action_dispatch.rack_cache = false
|
||||
config.action_dispatch.http_auth_salt = 'http authentication'
|
||||
config.action_dispatch.signed_cookie_salt = 'signed cookie'
|
||||
config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie'
|
||||
config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie'
|
||||
|
||||
config.action_dispatch.default_headers = {
|
||||
'X-Frame-Options' => 'SAMEORIGIN',
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
require 'abstract_unit'
|
||||
# FIXME remove DummyKeyGenerator and this require in 4.1
|
||||
require 'active_support/key_generator'
|
||||
|
||||
class FlashTest < ActionController::TestCase
|
||||
class TestController < ActionController::Base
|
||||
|
@ -217,7 +219,7 @@ end
|
|||
|
||||
class FlashIntegrationTest < ActionDispatch::IntegrationTest
|
||||
SessionKey = '_myapp_session'
|
||||
SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
|
||||
Generator = ActiveSupport::DummyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
|
||||
|
||||
class TestController < ActionController::Base
|
||||
add_flash_types :bar
|
||||
|
@ -291,7 +293,7 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
|
|||
|
||||
# Overwrite get to send SessionSecret in env hash
|
||||
def get(path, parameters = nil, env = {})
|
||||
env["action_dispatch.secret_token"] ||= SessionSecret
|
||||
env["action_dispatch.key_generator"] ||= Generator
|
||||
super
|
||||
end
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
require 'abstract_unit'
|
||||
# FIXME remove DummyKeyGenerator and this require in 4.1
|
||||
require 'active_support/key_generator'
|
||||
|
||||
class HttpDigestAuthenticationTest < ActionController::TestCase
|
||||
class DummyDigestController < ActionController::Base
|
||||
|
@ -40,8 +42,8 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
|
|||
|
||||
setup do
|
||||
# Used as secret in generating nonce to prevent tampering of timestamp
|
||||
@secret = "session_options_secret"
|
||||
@request.env["action_dispatch.secret_token"] = @secret
|
||||
@secret = "4fb45da9e4ab4ddeb7580d6a35503d99"
|
||||
@request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new(@secret)
|
||||
end
|
||||
|
||||
teardown do
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
require 'abstract_unit'
|
||||
# FIXME remove DummyKeyGenerator and this require in 4.1
|
||||
require 'active_support/key_generator'
|
||||
|
||||
class CookiesTest < ActionController::TestCase
|
||||
class TestController < ActionController::Base
|
||||
|
@ -65,6 +67,11 @@ class CookiesTest < ActionController::TestCase
|
|||
head :ok
|
||||
end
|
||||
|
||||
def set_encrypted_cookie
|
||||
cookies.encrypted[:foo] = 'bar'
|
||||
head :ok
|
||||
end
|
||||
|
||||
def raise_data_overflow
|
||||
cookies.signed[:foo] = 'bye!' * 1024
|
||||
head :ok
|
||||
|
@ -146,7 +153,10 @@ class CookiesTest < ActionController::TestCase
|
|||
|
||||
def setup
|
||||
super
|
||||
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
|
||||
@request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
|
||||
@request.env["action_dispatch.signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
|
||||
@request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
|
||||
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
|
||||
@request.host = "www.nextangle.com"
|
||||
end
|
||||
|
||||
|
@ -296,6 +306,16 @@ class CookiesTest < ActionController::TestCase
|
|||
assert_equal 45, @controller.send(:cookies).signed[:user_id]
|
||||
end
|
||||
|
||||
def test_encrypted_cookie
|
||||
get :set_encrypted_cookie
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 'bar', cookies[:foo]
|
||||
assert_raises TypeError do
|
||||
cookies.signed[:foo]
|
||||
end
|
||||
assert_equal 'bar', cookies.encrypted[:foo]
|
||||
end
|
||||
|
||||
def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature
|
||||
get :set_signed_cookie
|
||||
assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
|
||||
|
@ -329,29 +349,29 @@ class CookiesTest < ActionController::TestCase
|
|||
|
||||
def test_raises_argument_error_if_missing_secret
|
||||
assert_raise(ArgumentError, nil.inspect) {
|
||||
@request.env["action_dispatch.secret_token"] = nil
|
||||
@request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new(nil)
|
||||
get :set_signed_cookie
|
||||
}
|
||||
|
||||
assert_raise(ArgumentError, ''.inspect) {
|
||||
@request.env["action_dispatch.secret_token"] = ""
|
||||
@request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("")
|
||||
get :set_signed_cookie
|
||||
}
|
||||
end
|
||||
|
||||
def test_raises_argument_error_if_secret_is_probably_insecure
|
||||
assert_raise(ArgumentError, "password".inspect) {
|
||||
@request.env["action_dispatch.secret_token"] = "password"
|
||||
@request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("password")
|
||||
get :set_signed_cookie
|
||||
}
|
||||
|
||||
assert_raise(ArgumentError, "secret".inspect) {
|
||||
@request.env["action_dispatch.secret_token"] = "secret"
|
||||
@request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("secret")
|
||||
get :set_signed_cookie
|
||||
}
|
||||
|
||||
assert_raise(ArgumentError, "12345678901234567890123456789".inspect) {
|
||||
@request.env["action_dispatch.secret_token"] = "12345678901234567890123456789"
|
||||
@request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("12345678901234567890123456789")
|
||||
get :set_signed_cookie
|
||||
}
|
||||
end
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
require 'abstract_unit'
|
||||
require 'stringio'
|
||||
# FIXME remove DummyKeyGenerator and this require in 4.1
|
||||
require 'active_support/key_generator'
|
||||
|
||||
class CookieStoreTest < ActionDispatch::IntegrationTest
|
||||
SessionKey = '_myapp_session'
|
||||
SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
|
||||
Generator = ActiveSupport::DummyKeyGenerator.new(SessionSecret)
|
||||
|
||||
Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, :digest => 'SHA1')
|
||||
SignedBar = Verifier.generate(:foo => "bar", :session_id => SecureRandom.hex(16))
|
||||
|
@ -330,7 +333,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
|
|||
|
||||
# Overwrite get to send SessionSecret in env hash
|
||||
def get(path, parameters = nil, env = {})
|
||||
env["action_dispatch.secret_token"] ||= SessionSecret
|
||||
env["action_dispatch.key_generator"] ||= Generator
|
||||
super
|
||||
end
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
require 'mutex_m'
|
||||
require 'openssl'
|
||||
|
||||
module ActiveSupport
|
||||
|
@ -20,4 +21,51 @@ module ActiveSupport
|
|||
OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
|
||||
end
|
||||
end
|
||||
|
||||
class CachingKeyGenerator
|
||||
def initialize(key_generator)
|
||||
@key_generator = key_generator
|
||||
@cache_keys = {}.extend(Mutex_m)
|
||||
end
|
||||
|
||||
def generate_key(salt, key_size=64)
|
||||
@cache_keys.synchronize do
|
||||
@cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class DummyKeyGenerator
|
||||
SECRET_MIN_LENGTH = 30 # Characters
|
||||
|
||||
def initialize(secret)
|
||||
ensure_secret_secure(secret)
|
||||
@secret = secret
|
||||
end
|
||||
|
||||
def generate_key(salt)
|
||||
@secret
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# To prevent users from using something insecure like "Password" we make sure that the
|
||||
# secret they've provided is at least 30 characters in length.
|
||||
def ensure_secret_secure(secret)
|
||||
if secret.blank?
|
||||
raise ArgumentError, "A secret is required to generate an " +
|
||||
"integrity hash for cookie session data. Use " +
|
||||
"config.secret_key_base = \"some secret phrase of at " +
|
||||
"least #{SECRET_MIN_LENGTH} characters\"" +
|
||||
"in config/initializers/secret_token.rb"
|
||||
end
|
||||
|
||||
if secret.length < SECRET_MIN_LENGTH
|
||||
raise ArgumentError, "Secret should be something secure, " +
|
||||
"like \"#{SecureRandom.hex(16)}\". The value you " +
|
||||
"provided, \"#{secret}\", is shorter than the minimum length " +
|
||||
"of #{SECRET_MIN_LENGTH} characters"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -39,10 +39,13 @@ module ActiveSupport
|
|||
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
|
||||
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
|
||||
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
|
||||
def initialize(secret, options = {})
|
||||
def initialize(secret, *signature_key_or_options)
|
||||
options = signature_key_or_options.extract_options!
|
||||
sign_secret = signature_key_or_options.first
|
||||
@secret = secret
|
||||
@sign_secret = sign_secret
|
||||
@cipher = options[:cipher] || 'aes-256-cbc'
|
||||
@verifier = MessageVerifier.new(@secret, :serializer => NullSerializer)
|
||||
@verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
|
||||
@serializer = options[:serializer] || Marshal
|
||||
end
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
def test_alternative_serialization_method
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), :serializer => JSONSerializer.new)
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), SecureRandom.hex(64), :serializer => JSONSerializer.new)
|
||||
message = encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) })
|
||||
assert_equal encryptor.decrypt_and_verify(message), { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
|
||||
end
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
# no regular words or you'll be exposed to dictionary attacks.
|
||||
# Make sure your secret key is kept private
|
||||
# if you're sharing your code publicly.
|
||||
Blog::Application.config.secret_token = '685a9bf865b728c6549a191c90851c1b5ec41ecb60b9e94ad79dd3f824749798aa7b5e94431901960bee57809db0947b481570f7f13376b7ca190fa28099c459'
|
||||
Blog::Application.config.secret_key_base = '685a9bf865b728c6549a191c90851c1b5ec41ecb60b9e94ad79dd3f824749798aa7b5e94431901960bee57809db0947b481570f7f13376b7ca190fa28099c459'
|
||||
|
|
|
@ -219,7 +219,7 @@ Rails sets up (for the CookieStore) a secret key used for signing the session da
|
|||
# If you change this key, all old signed cookies will become invalid!
|
||||
# Make sure the secret is at least 30 characters and all random,
|
||||
# no regular words or you'll be exposed to dictionary attacks.
|
||||
YourApp::Application.config.secret_token = '49d3f3de9ed86c74b94ad6bd0...'
|
||||
YourApp::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...'
|
||||
```
|
||||
|
||||
NOTE: Changing the secret when using the `CookieStore` will invalidate all existing sessions.
|
||||
|
|
|
@ -113,7 +113,7 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
|
|||
|
||||
* `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If `config.cache_classes` is true, this option is ignored.
|
||||
|
||||
* `config.secret_token` used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `config.secret_token` initialized to a random key in `config/initializers/secret_token.rb`.
|
||||
* `config.secret_key_base` used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `config.secret_key_base` initialized to a random key in `config/initializers/secret_token.rb`.
|
||||
|
||||
* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. Nginx or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won´t be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app.
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
require 'fileutils'
|
||||
require 'active_support/queueing'
|
||||
# FIXME remove DummyKeyGenerator and this require in 4.1
|
||||
require 'active_support/key_generator'
|
||||
require 'rails/engine'
|
||||
|
||||
module Rails
|
||||
|
@ -106,32 +108,57 @@ module Rails
|
|||
def key_generator
|
||||
# number of iterations selected based on consultation with the google security
|
||||
# team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220
|
||||
@key_generator ||= ActiveSupport::KeyGenerator.new(config.secret_token, iterations: 1000)
|
||||
@caching_key_generator ||= begin
|
||||
if config.secret_key_base
|
||||
key_generator = ActiveSupport::KeyGenerator.new(config.secret_key_base, iterations: 1000)
|
||||
ActiveSupport::CachingKeyGenerator.new(key_generator)
|
||||
else
|
||||
ActiveSupport::DummyKeyGenerator.new(config.secret_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Stores some of the Rails initial environment parameters which
|
||||
# will be used by middlewares and engines to configure themselves.
|
||||
# Currently stores:
|
||||
#
|
||||
# * "action_dispatch.parameter_filter" => config.filter_parameters,
|
||||
# * "action_dispatch.secret_token" => config.secret_token,
|
||||
# * "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
|
||||
# * "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
|
||||
# * "action_dispatch.logger" => Rails.logger,
|
||||
# * "action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner
|
||||
# * "action_dispatch.parameter_filter" => config.filter_parameters
|
||||
# * "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions
|
||||
# * "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local
|
||||
# * "action_dispatch.logger" => Rails.logger
|
||||
# * "action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner
|
||||
# * "action_dispatch.key_generator" => key_generator
|
||||
# * "action_dispatch.http_auth_salt" => config.action_dispatch.http_auth_salt
|
||||
# * "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt
|
||||
# * "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt
|
||||
# * "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt
|
||||
#
|
||||
# These parameters will be used by middlewares and engines to configure themselves
|
||||
#
|
||||
def env_config
|
||||
@env_config ||= super.merge({
|
||||
"action_dispatch.parameter_filter" => config.filter_parameters,
|
||||
"action_dispatch.secret_token" => config.secret_token,
|
||||
"action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
|
||||
"action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
|
||||
"action_dispatch.logger" => Rails.logger,
|
||||
"action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner,
|
||||
"action_dispatch.key_generator" => key_generator
|
||||
})
|
||||
@env_config ||= begin
|
||||
if config.secret_key_base.nil?
|
||||
ActiveSupport::Deprecation.warn "You didn't set config.secret_key_base. " +
|
||||
"This should be used instead of the old deprecated config.secret_token. " +
|
||||
"Set config.secret_key_base instead of config.secret_token in config/initializers/secret_token.rb"
|
||||
if config.secret_token.blank?
|
||||
raise "You must set config.secret_key_base in your app's config"
|
||||
end
|
||||
end
|
||||
|
||||
super.merge({
|
||||
"action_dispatch.parameter_filter" => config.filter_parameters,
|
||||
"action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
|
||||
"action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
|
||||
"action_dispatch.logger" => Rails.logger,
|
||||
"action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner,
|
||||
"action_dispatch.key_generator" => key_generator,
|
||||
"action_dispatch.http_auth_salt" => config.action_dispatch.http_auth_salt,
|
||||
"action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt,
|
||||
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
|
||||
"action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
## Rails internal API
|
||||
|
|
|
@ -10,7 +10,7 @@ module Rails
|
|||
:cache_classes, :cache_store, :consider_all_requests_local, :console,
|
||||
:eager_load, :exceptions_app, :file_watcher, :filter_parameters,
|
||||
:force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags,
|
||||
:railties_order, :relative_url_root, :secret_token,
|
||||
:railties_order, :relative_url_root, :secret_key_base, :secret_token,
|
||||
:serve_static_assets, :ssl_options, :static_cache_control, :session_options,
|
||||
:time_zone, :reload_classes_only_on_change,
|
||||
:queue, :queue_consumer, :beginning_of_week
|
||||
|
@ -46,6 +46,8 @@ module Rails
|
|||
@queue = ActiveSupport::SynchronousQueue.new
|
||||
@queue_consumer = nil
|
||||
@eager_load = nil
|
||||
@secret_token = nil
|
||||
@secret_key_base = nil
|
||||
|
||||
@assets = ActiveSupport::OrderedOptions.new
|
||||
@assets.enabled = false
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
# no regular words or you'll be exposed to dictionary attacks.
|
||||
# You can use `rake secret` to generate a secure secret key.
|
||||
|
||||
# Make sure your secret_token is kept private
|
||||
# Make sure your secret_key_base is kept private
|
||||
# if you're sharing your code publicly.
|
||||
<%= app_const %>.config.secret_token = '<%= app_secret %>'
|
||||
<%= app_const %>.config.secret_key_base = '<%= app_secret %>'
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
<%= app_const %>.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>
|
||||
<%= app_const %>.config.session_store :encrypted_cookie_store, key: <%= "'_#{app_name}_session'" %>
|
||||
|
|
|
@ -14,5 +14,6 @@ require 'rails/all'
|
|||
module TestApp
|
||||
class Application < Rails::Application
|
||||
config.root = File.dirname(__FILE__)
|
||||
config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -225,21 +225,24 @@ module ApplicationTests
|
|||
assert_equal Pathname.new(app_path).join("somewhere"), Rails.public_path
|
||||
end
|
||||
|
||||
test "config.secret_token is sent in env" do
|
||||
test "Use key_generator when secret_key_base is set" do
|
||||
make_basic_app do |app|
|
||||
app.config.secret_token = 'b3c631c314c0bbca50c1b2843150fe33'
|
||||
app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
|
||||
app.config.session_store :disabled
|
||||
end
|
||||
|
||||
class ::OmgController < ActionController::Base
|
||||
def index
|
||||
cookies.signed[:some_key] = "some_value"
|
||||
render text: env["action_dispatch.secret_token"]
|
||||
render text: cookies[:some_key]
|
||||
end
|
||||
end
|
||||
|
||||
get "/"
|
||||
assert_equal 'b3c631c314c0bbca50c1b2843150fe33', last_response.body
|
||||
|
||||
secret = app.key_generator.generate_key('signed cookie')
|
||||
verifier = ActiveSupport::MessageVerifier.new(secret)
|
||||
assert_equal 'some_value', verifier.verify(last_response.body)
|
||||
end
|
||||
|
||||
test "protect from forgery is the default in a new app" do
|
||||
|
@ -568,7 +571,6 @@ module ApplicationTests
|
|||
|
||||
assert_respond_to app, :env_config
|
||||
assert_equal app.env_config['action_dispatch.parameter_filter'], app.config.filter_parameters
|
||||
assert_equal app.env_config['action_dispatch.secret_token'], app.config.secret_token
|
||||
assert_equal app.env_config['action_dispatch.show_exceptions'], app.config.action_dispatch.show_exceptions
|
||||
assert_equal app.env_config['action_dispatch.logger'], Rails.logger
|
||||
assert_equal app.env_config['action_dispatch.backtrace_cleaner'], Rails.backtrace_cleaner
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
require 'isolation/abstract_unit'
|
||||
# FIXME remove DummyKeyGenerator and this require in 4.1
|
||||
require 'active_support/key_generator'
|
||||
|
||||
module ApplicationTests
|
||||
class RemoteIpTest < ActiveSupport::TestCase
|
||||
|
@ -8,7 +10,7 @@ module ApplicationTests
|
|||
remote_ip = nil
|
||||
env = Rack::MockRequest.env_for("/").merge(env).merge!(
|
||||
'action_dispatch.show_exceptions' => false,
|
||||
'action_dispatch.secret_token' => 'b3c631c314c0bbca50c1b2843150fe33'
|
||||
'action_dispatch.key_generator' => ActiveSupport::DummyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
|
||||
)
|
||||
|
||||
endpoint = Proc.new do |e|
|
||||
|
|
|
@ -128,5 +128,56 @@ module ApplicationTests
|
|||
get '/foo/read_cookie' # Cookie shouldn't be changed
|
||||
assert_equal '"1"', last_response.body
|
||||
end
|
||||
|
||||
test "session using encrypted cookie store" do
|
||||
app_file 'config/routes.rb', <<-RUBY
|
||||
AppTemplate::Application.routes.draw do
|
||||
get ':controller(/:action)'
|
||||
end
|
||||
RUBY
|
||||
|
||||
controller :foo, <<-RUBY
|
||||
class FooController < ActionController::Base
|
||||
def write_session
|
||||
session[:foo] = 1
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def read_session
|
||||
render text: session[:foo]
|
||||
end
|
||||
|
||||
def read_encrypted_cookie
|
||||
render text: cookies.encrypted[:_myapp_session]['foo']
|
||||
end
|
||||
|
||||
def read_raw_cookie
|
||||
render text: cookies[:_myapp_session]
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
add_to_config <<-RUBY
|
||||
config.session_store :encrypted_cookie_store, key: '_myapp_session'
|
||||
config.action_dispatch.derive_signed_cookie_key = true
|
||||
RUBY
|
||||
|
||||
require "#{app_path}/config/environment"
|
||||
|
||||
get '/foo/write_session'
|
||||
get '/foo/write_session'
|
||||
get '/foo/read_session'
|
||||
assert_equal '1', last_response.body
|
||||
|
||||
get '/foo/read_encrypted_cookie'
|
||||
assert_equal '1', last_response.body
|
||||
|
||||
secret = app.key_generator.generate_key('encrypted cookie')
|
||||
sign_secret = app.key_generator.generate_key('signed encrypted cookie')
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
|
||||
|
||||
get '/foo/read_raw_cookie'
|
||||
assert_equal 1, encryptor.decrypt_and_verify(last_response.body)['foo']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ module ApplicationTests
|
|||
require "action_controller/railtie"
|
||||
|
||||
class MyApp < Rails::Application
|
||||
config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
|
||||
config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
|
||||
config.session_store :cookie_store, key: "_myapp_session"
|
||||
config.active_support.deprecation = :log
|
||||
config.eager_load = false
|
||||
|
|
|
@ -341,7 +341,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
|
|||
def test_new_hash_style
|
||||
run_generator [destination_root]
|
||||
assert_file "config/initializers/session_store.rb" do |file|
|
||||
assert_match(/config.session_store :cookie_store, key: '_.+_session'/, file)
|
||||
assert_match(/config.session_store :encrypted_cookie_store, key: '_.+_session'/, file)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ module TestHelpers
|
|||
|
||||
add_to_config <<-RUBY
|
||||
config.eager_load = false
|
||||
config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
|
||||
config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
|
||||
config.session_store :cookie_store, key: "_myapp_session"
|
||||
config.active_support.deprecation = :log
|
||||
config.action_controller.allow_forgery_protection = false
|
||||
|
@ -138,7 +138,7 @@ module TestHelpers
|
|||
|
||||
app = Class.new(Rails::Application)
|
||||
app.config.eager_load = false
|
||||
app.config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
|
||||
app.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
|
||||
app.config.session_store :cookie_store, key: "_myapp_session"
|
||||
app.config.active_support.deprecation = :log
|
||||
|
||||
|
|
Loading…
Reference in a new issue