mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Purpose Metadata For Signed And Encrypted Cookies
Purpose metadata prevents cookie values from being copy-pasted and ensures that the cookie is used only for its originally intended purpose. The Purpose and Expiry metadata are embedded inside signed/encrypted cookies and will not be readable on previous versions of Rails. We can switch off purpose and expiry metadata embedded in signed and encrypted cookies using config.action_dispatch.use_cookies_with_metadata = false if you want your cookies to be readable on older versions of Rails.
This commit is contained in:
parent
ba1dab1e3b
commit
1cda4fb5df
8 changed files with 218 additions and 23 deletions
|
@ -81,6 +81,10 @@ module ActionDispatch
|
|||
get_header Cookies::COOKIES_ROTATIONS
|
||||
end
|
||||
|
||||
def use_cookies_with_metadata
|
||||
get_header Cookies::USE_COOKIES_WITH_METADATA
|
||||
end
|
||||
|
||||
# :startdoc:
|
||||
end
|
||||
|
||||
|
@ -182,6 +186,7 @@ module ActionDispatch
|
|||
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
|
||||
COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
|
||||
COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze
|
||||
USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata".freeze
|
||||
|
||||
# Cookies can typically store 4096 bytes.
|
||||
MAX_COOKIE_SIZE = 4096
|
||||
|
@ -470,7 +475,7 @@ module ActionDispatch
|
|||
|
||||
def [](name)
|
||||
if data = @parent_jar[name.to_s]
|
||||
parse name, data
|
||||
parse(name, data, purpose: "cookie.#{name}") || parse(name, data)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -481,7 +486,7 @@ module ActionDispatch
|
|||
options = { value: options }
|
||||
end
|
||||
|
||||
commit(options)
|
||||
commit(name, options)
|
||||
@parent_jar[name] = options
|
||||
end
|
||||
|
||||
|
@ -497,13 +502,24 @@ module ActionDispatch
|
|||
end
|
||||
end
|
||||
|
||||
def parse(name, data); data; end
|
||||
def commit(options); end
|
||||
def cookie_metadata(name, options)
|
||||
if request.use_cookies_with_metadata
|
||||
metadata = expiry_options(options)
|
||||
metadata[:purpose] = "cookie.#{name}"
|
||||
|
||||
metadata
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def parse(name, data, purpose: nil); data; end
|
||||
def commit(name, options); end
|
||||
end
|
||||
|
||||
class PermanentCookieJar < AbstractCookieJar # :nodoc:
|
||||
private
|
||||
def commit(options)
|
||||
def commit(name, options)
|
||||
options[:expires] = 20.years.from_now
|
||||
end
|
||||
end
|
||||
|
@ -583,14 +599,14 @@ module ActionDispatch
|
|||
end
|
||||
|
||||
private
|
||||
def parse(name, signed_message)
|
||||
def parse(name, signed_message, purpose: nil)
|
||||
deserialize(name) do |rotate|
|
||||
@verifier.verified(signed_message, on_rotation: rotate)
|
||||
@verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
|
||||
end
|
||||
end
|
||||
|
||||
def commit(options)
|
||||
options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options))
|
||||
def commit(name, options)
|
||||
options[:value] = @verifier.generate(serialize(options[:value]), cookie_metadata(name, options))
|
||||
|
||||
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
|
||||
end
|
||||
|
@ -631,16 +647,16 @@ module ActionDispatch
|
|||
end
|
||||
|
||||
private
|
||||
def parse(name, encrypted_message)
|
||||
def parse(name, encrypted_message, purpose: nil)
|
||||
deserialize(name) do |rotate|
|
||||
@encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
|
||||
@encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose)
|
||||
end
|
||||
rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
|
||||
parse_legacy_signed_message(name, encrypted_message)
|
||||
end
|
||||
|
||||
def commit(options)
|
||||
options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options))
|
||||
def commit(name, options)
|
||||
options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), cookie_metadata(name, options))
|
||||
|
||||
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
|
||||
end
|
||||
|
|
|
@ -21,6 +21,7 @@ module ActionDispatch
|
|||
config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie"
|
||||
config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie"
|
||||
config.action_dispatch.use_authenticated_cookie_encryption = false
|
||||
config.action_dispatch.use_cookies_with_metadata = false
|
||||
config.action_dispatch.perform_deep_munge = true
|
||||
|
||||
config.action_dispatch.default_headers = {
|
||||
|
|
|
@ -289,6 +289,46 @@ class CookiesTest < ActionController::TestCase
|
|||
cookies[:user_name] = { value: "assain", expires: 2.hours }
|
||||
head :ok
|
||||
end
|
||||
|
||||
def encrypted_discount_and_user_id_cookie
|
||||
cookies.encrypted[:user_id] = { value: 50, expires: 1.hour }
|
||||
cookies.encrypted[:discount_percentage] = 10
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def signed_discount_and_user_id_cookie
|
||||
cookies.signed[:user_id] = { value: 50, expires: 1.hour }
|
||||
cookies.signed[:discount_percentage] = 10
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on
|
||||
# cookies.encrypted[:favorite] = { value: "5-2-Stable Chocolate Cookies", expires: 1000.years }
|
||||
cookies[:favorite] = "KvH5lIHvX5vPQkLIK63r/NuIMwzWky8M0Zwk8SZ6DwUv8+srf36geR4nWq5KmhsZIYXA8NRdCZYIfxMKJsOFlz77Gf+Fq8vBBCWJTp95rx39A28TCUTJEyMhCNJO5eie7Skef76Qt5Jo/SCnIADAhzyGQkGBopKRcA==--qXZZFWGbCy6N8AGy--WswoH+xHrNh9MzSXDpB2fA=="
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off
|
||||
cookies[:favorite] = "Wmg4amgvcVVvWGcwK3c4WjJEbTdRQUgrWXhBdDliUTR0cVNidXpmVTMrc2RjcitwUzVsWWEwZGtuVGtFUjJwNi0tcVhVMTFMOTQ1d0hIVE1FK0pJc05SQT09--8b2a55c375049a50f7a959b9d42b31ef0b2bb594"
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on
|
||||
# cookies.signed[:favorite] = { value: "5-2-Stable Choco Chip Cookie", expires: 1000.years }
|
||||
cookies[:favorite] = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaUUxTFRJdFUzUmhZbXhsSUVOb2IyTnZJRU5vYVhBZ1EyOXZhMmxsQmpvR1JWUT0iLCJleHAiOiIzMDE4LTA3LTExVDE2OjExOjI2Ljc1M1oiLCJwdXIiOm51bGx9fQ==--7df5d885b78b70a501d6e82140ae91b24060ac00"
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off
|
||||
cookies[:favorite] = "BAhJIiE1LTItU3RhYmxlIENob2NvIENoaXAgQ29va2llBjoGRVQ=--50bbdbf8d64f5a3ec3e54878f54d4f55b6cb3aff"
|
||||
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
tests TestController
|
||||
|
@ -1274,6 +1314,8 @@ class CookiesTest < ActionController::TestCase
|
|||
end
|
||||
|
||||
def test_signed_cookie_with_expires_set_relatively
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = true
|
||||
|
||||
cookies.signed[:user_name] = { value: "assain", expires: 2.hours }
|
||||
|
||||
travel 1.hour
|
||||
|
@ -1284,6 +1326,8 @@ class CookiesTest < ActionController::TestCase
|
|||
end
|
||||
|
||||
def test_encrypted_cookie_with_expires_set_relatively
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = true
|
||||
|
||||
cookies.encrypted[:user_name] = { value: "assain", expires: 2.hours }
|
||||
|
||||
travel 1.hour
|
||||
|
@ -1300,6 +1344,128 @@ class CookiesTest < ActionController::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_purpose_metadata_for_encrypted_cookies
|
||||
get :encrypted_discount_and_user_id_cookie
|
||||
|
||||
cookies[:discount_percentage] = cookies[:user_id]
|
||||
assert_equal 50, cookies.encrypted[:discount_percentage]
|
||||
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = true
|
||||
|
||||
get :encrypted_discount_and_user_id_cookie
|
||||
|
||||
cookies[:discount_percentage] = cookies[:user_id]
|
||||
assert_nil cookies.encrypted[:discount_percentage]
|
||||
end
|
||||
|
||||
def test_purpose_metadata_for_signed_cookies
|
||||
get :signed_discount_and_user_id_cookie
|
||||
|
||||
cookies[:discount_percentage] = cookies[:user_id]
|
||||
assert_equal 50, cookies.signed[:discount_percentage]
|
||||
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = true
|
||||
|
||||
get :signed_discount_and_user_id_cookie
|
||||
|
||||
cookies[:discount_percentage] = cookies[:user_id]
|
||||
assert_nil cookies.signed[:discount_percentage]
|
||||
end
|
||||
|
||||
def test_switch_off_metadata_for_encrypted_cookies_if_config_is_false
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = false
|
||||
|
||||
get :encrypted_discount_and_user_id_cookie
|
||||
|
||||
travel 2.hours
|
||||
assert_equal 50, cookies.encrypted[:user_id]
|
||||
|
||||
cookies[:discount_percentage] = cookies[:user_id]
|
||||
assert_not_equal 10, cookies.encrypted[:discount_percentage]
|
||||
assert_equal 50, cookies.encrypted[:discount_percentage]
|
||||
end
|
||||
|
||||
def test_switch_off_metadata_for_signed_cookies_if_config_is_false
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = false
|
||||
|
||||
get :signed_discount_and_user_id_cookie
|
||||
|
||||
travel 2.hours
|
||||
assert_equal 50, cookies.signed[:user_id]
|
||||
|
||||
cookies[:discount_percentage] = cookies[:user_id]
|
||||
assert_not_equal 10, cookies.signed[:discount_percentage]
|
||||
assert_equal 50, cookies.signed[:discount_percentage]
|
||||
end
|
||||
|
||||
def test_read_rails_5_2_stable_encrypted_cookies_if_config_is_false
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = false
|
||||
|
||||
get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on
|
||||
|
||||
assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite]
|
||||
|
||||
freeze_time do
|
||||
travel 1001.years
|
||||
assert_nil cookies.encrypted[:favorite]
|
||||
end
|
||||
|
||||
get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off
|
||||
|
||||
assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite]
|
||||
end
|
||||
|
||||
def test_read_rails_5_2_stable_signed_cookies_if_config_is_false
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = false
|
||||
|
||||
get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on
|
||||
|
||||
assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite]
|
||||
|
||||
freeze_time do
|
||||
travel 1001.years
|
||||
assert_nil cookies.signed[:favorite]
|
||||
end
|
||||
|
||||
get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off
|
||||
|
||||
assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite]
|
||||
end
|
||||
|
||||
def test_read_rails_5_2_stable_encrypted_cookies_if_use_metadata_config_is_true
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = true
|
||||
|
||||
get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on
|
||||
|
||||
assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite]
|
||||
|
||||
freeze_time do
|
||||
travel 1001.years
|
||||
assert_nil cookies.encrypted[:favorite]
|
||||
end
|
||||
|
||||
get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off
|
||||
|
||||
assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite]
|
||||
end
|
||||
|
||||
def test_read_rails_5_2_stable_signed_cookies_if_use_metadata_config_is_true
|
||||
request.env["action_dispatch.use_cookies_with_metadata"] = true
|
||||
|
||||
get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on
|
||||
|
||||
assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite]
|
||||
|
||||
freeze_time do
|
||||
travel 1001.years
|
||||
assert_nil cookies.signed[:favorite]
|
||||
end
|
||||
|
||||
get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off
|
||||
|
||||
assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite]
|
||||
end
|
||||
|
||||
private
|
||||
def assert_cookie_header(expected)
|
||||
header = @response.headers["Set-Cookie"]
|
||||
|
|
|
@ -267,6 +267,7 @@ module Rails
|
|||
"action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer,
|
||||
"action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest,
|
||||
"action_dispatch.cookies_rotations" => config.action_dispatch.cookies_rotations,
|
||||
"action_dispatch.use_cookies_with_metadata" => config.action_dispatch.use_cookies_with_metadata,
|
||||
"action_dispatch.content_security_policy" => config.content_security_policy,
|
||||
"action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only,
|
||||
"action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator
|
||||
|
|
|
@ -120,6 +120,10 @@ module Rails
|
|||
if respond_to?(:action_view)
|
||||
action_view.default_enforce_utf8 = false
|
||||
end
|
||||
|
||||
if respond_to?(:action_dispatch)
|
||||
action_dispatch.use_cookies_with_metadata = true
|
||||
end
|
||||
else
|
||||
raise "Unknown version #{target_version.to_s.inspect}"
|
||||
end
|
||||
|
|
|
@ -8,3 +8,10 @@
|
|||
|
||||
# Don't force requests from old versions of IE to be UTF-8 encoded
|
||||
# Rails.application.config.action_view.default_enforce_utf8 = false
|
||||
|
||||
# Embed purpose and expiry metadata inside signed and encrypted
|
||||
# cookies for increased security.
|
||||
#
|
||||
# This option is not backwards compatible with earlier Rails versions.
|
||||
# It's best enabled when your entire app is migrated and stable on 6.0.
|
||||
# Rails.application.config.action_dispatch.use_cookies_with_metadata = true
|
||||
|
|
|
@ -110,14 +110,14 @@ module ApplicationTests
|
|||
assert_equal "signed cookie".inspect, last_response.body
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal "signed cookie", verifier_sha512.verify(last_response.body)
|
||||
assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie")
|
||||
|
||||
get "/foo/write_raw_cookie_sha256"
|
||||
get "/foo/read_signed"
|
||||
assert_equal "signed cookie".inspect, last_response.body
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal "signed cookie", verifier_sha512.verify(last_response.body)
|
||||
assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie")
|
||||
end
|
||||
|
||||
test "encrypted cookies rotating multiple encryption keys" do
|
||||
|
@ -180,14 +180,14 @@ module ApplicationTests
|
|||
assert_equal "encrypted cookie".inspect, last_response.body
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body)
|
||||
assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie")
|
||||
|
||||
get "/foo/write_raw_cookie_sha256"
|
||||
get "/foo/write_raw_cookie_two"
|
||||
get "/foo/read_encrypted"
|
||||
assert_equal "encrypted cookie".inspect, last_response.body
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body)
|
||||
assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -183,7 +183,7 @@ module ApplicationTests
|
|||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"]
|
||||
assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
|
||||
end
|
||||
|
||||
test "session upgrading signature to encryption cookie store works the same way as encrypted cookie store" do
|
||||
|
@ -235,7 +235,7 @@ module ApplicationTests
|
|||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"]
|
||||
assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
|
||||
end
|
||||
|
||||
test "session upgrading signature to encryption cookie store upgrades session to encrypted mode" do
|
||||
|
@ -297,7 +297,7 @@ module ApplicationTests
|
|||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"]
|
||||
assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
|
||||
end
|
||||
|
||||
test "session upgrading from AES-CBC-HMAC encryption to AES-GCM encryption" do
|
||||
|
@ -364,7 +364,7 @@ module ApplicationTests
|
|||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"]
|
||||
assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
|
||||
ensure
|
||||
ENV["RAILS_ENV"] = old_rails_env
|
||||
end
|
||||
|
@ -428,7 +428,7 @@ module ApplicationTests
|
|||
verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token)
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal 2, verifier.verify(last_response.body)["foo"]
|
||||
assert_equal 2, verifier.verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
|
||||
ensure
|
||||
ENV["RAILS_ENV"] = old_rails_env
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue