1
0
Fork 0
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:
Santiago Pastorino 2012-11-15 12:17:25 -08:00
commit ef8b845de7
27 changed files with 321 additions and 92 deletions

View file

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

View file

@ -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(*)

View file

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

View file

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

View file

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

View file

@ -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',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 %>'

View file

@ -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'" %>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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