aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb6
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb8
-rw-r--r--actionpack/lib/action_dispatch.rb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb106
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb23
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb4
-rw-r--r--actionpack/test/controller/flash_test.rb6
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb6
-rw-r--r--actionpack/test/dispatch/cookies_test.rb32
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb5
-rw-r--r--activesupport/lib/active_support/key_generator.rb48
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb7
-rw-r--r--activesupport/test/message_encryptor_test.rb2
-rw-r--r--guides/code/getting_started/config/initializers/secret_token.rb2
-rw-r--r--guides/source/action_controller_overview.md2
-rw-r--r--guides/source/configuring.md2
-rw-r--r--railties/lib/rails/application.rb59
-rw-r--r--railties/lib/rails/application/configuration.rb4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt2
-rw-r--r--railties/test/abstract_unit.rb1
-rw-r--r--railties/test/application/configuration_test.rb12
-rw-r--r--railties/test/application/middleware/remote_ip_test.rb4
-rw-r--r--railties/test/application/middleware/session_test.rb51
-rw-r--r--railties/test/application/url_generation_test.rb2
-rw-r--r--railties/test/generators/app_generator_test.rb2
-rw-r--r--railties/test/isolation/abstract_unit.rb4
27 files changed, 321 insertions, 92 deletions
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 6d46586367..d3b5bafee1 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -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.
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index a50f0ca8c1..265ce5d6f3 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -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(*)
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 0ec355246e..ab1755acd5 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -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
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index eaf922595a..7936dcb515 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -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
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index 3f28ea75ef..039846688e 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -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
+ end
+ end
+
+ class EncryptedCookieStore < CookieStore
+
+ private
+
+ def cookie_jar(env)
request = ActionDispatch::Request.new(env)
- request.cookie_jar.signed[@key] = cookie
+ request.cookie_jar.encrypted
end
end
end
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 284dd180db..98c87d9b2d 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -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',
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index 8340aab4d2..d8c78db8c0 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -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
diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb
index b11ad633bd..c4a94264c3 100644
--- a/actionpack/test/controller/http_digest_authentication_test.rb
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -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
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index 347b3b3b5a..ffa91d63c4 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -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
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index 41fa036a92..1677dee524 100644
--- a/actionpack/test/dispatch/session/cookie_store_test.rb
+++ b/actionpack/test/dispatch/session/cookie_store_test.rb
@@ -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
diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb
index 04d170f801..2c8fccec7d 100644
--- a/activesupport/lib/active_support/key_generator.rb
+++ b/activesupport/lib/active_support/key_generator.rb
@@ -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
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index 580267708c..1588674afc 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -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
diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb
index b544742300..d6c31396b6 100644
--- a/activesupport/test/message_encryptor_test.rb
+++ b/activesupport/test/message_encryptor_test.rb
@@ -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
diff --git a/guides/code/getting_started/config/initializers/secret_token.rb b/guides/code/getting_started/config/initializers/secret_token.rb
index f36ebdda18..969ecaad65 100644
--- a/guides/code/getting_started/config/initializers/secret_token.rb
+++ b/guides/code/getting_started/config/initializers/secret_token.rb
@@ -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'
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index dd8d229e6a..5be2991c4a 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -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.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 0b4f183d61..b5bedfb1b7 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -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.
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 9ef001c7d0..b7844bbfbe 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -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
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index cc21213f1c..f97e66985c 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -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
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt
index 3c5611ca59..e5caab3672 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt
@@ -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 %>'
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
index 4a099a4ce2..df07de9922 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
@@ -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'" %>
diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb
index dfcf5aa27d..2ea1d2aff4 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -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
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index c4c1100f19..b9d18f4582 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -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
diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb
index 9d97bae9ae..fde13eeb94 100644
--- a/railties/test/application/middleware/remote_ip_test.rb
+++ b/railties/test/application/middleware/remote_ip_test.rb
@@ -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|
diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb
index 5ce41caf61..b1a19d590c 100644
--- a/railties/test/application/middleware/session_test.rb
+++ b/railties/test/application/middleware/session_test.rb
@@ -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
diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb
index 2a48adae5c..0905757442 100644
--- a/railties/test/application/url_generation_test.rb
+++ b/railties/test/application/url_generation_test.rb
@@ -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
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 62e1f3531e..5ea31f2e0f 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -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
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index e59488f97d..0cb65f8e0d 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -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