1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Merge pull request #39076 from etiennebarrie/upgrade-safe-urlsafe-csrf-tokens

Upgrade-safe URL-safe CSRF tokens
This commit is contained in:
Rafael Mendonça França 2020-06-11 11:40:32 -04:00
commit 98a4c0c769
No known key found for this signature in database
GPG key ID: FC23B6D0F1EEE948
6 changed files with 71 additions and 8 deletions

View file

@ -83,7 +83,7 @@
out of the box: the value has to be url-encoded and decoded to survive transport.
Now, we generate Base64 urlsafe-encoded CSRF tokens, which are inherently safe
to transport. Validation accepts both urlsafe tokens, and strict-encoded tokens
to transport. Validation accepts both urlsafe tokens, and strict-encoded tokens
for backwards compatibility.
*Scott Blum*

View file

@ -90,6 +90,10 @@ module ActionController #:nodoc:
config_accessor :default_protect_from_forgery
self.default_protect_from_forgery = false
# Controls whether URL-safe CSRF tokens are generated.
config_accessor :urlsafe_csrf_tokens, instance_writer: false
self.urlsafe_csrf_tokens = false
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
@ -329,7 +333,7 @@ module ActionController #:nodoc:
end
begin
masked_token = Base64.urlsafe_decode64(encoded_masked_token)
masked_token = decode_csrf_token(encoded_masked_token)
rescue ArgumentError # encoded_masked_token is invalid Base64
return false
end
@ -367,7 +371,7 @@ module ActionController #:nodoc:
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
masked_token = one_time_pad + encrypted_csrf_token
Base64.urlsafe_encode64(masked_token, padding: false)
encode_csrf_token(masked_token)
end
def compare_with_real_token(token, session) # :doc:
@ -393,8 +397,8 @@ module ActionController #:nodoc:
end
def real_csrf_token(session) # :doc:
session[:_csrf_token] ||= SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH, padding: false)
Base64.urlsafe_decode64(session[:_csrf_token])
session[:_csrf_token] ||= generate_csrf_token
decode_csrf_token(session[:_csrf_token])
end
def per_form_csrf_token(session, action_path, method) # :doc:
@ -462,5 +466,33 @@ module ActionController #:nodoc:
uri = URI.parse(action_path)
uri.path.chomp("/")
end
def generate_csrf_token # :nodoc:
if urlsafe_csrf_tokens
SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH, padding: false)
else
SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
end
end
def encode_csrf_token(csrf_token) # :nodoc:
if urlsafe_csrf_tokens
Base64.urlsafe_encode64(csrf_token, padding: false)
else
Base64.strict_encode64(csrf_token)
end
end
def decode_csrf_token(encoded_csrf_token) # :nodoc:
if urlsafe_csrf_tokens
Base64.urlsafe_decode64(encoded_csrf_token)
else
begin
Base64.strict_decode64(encoded_csrf_token)
rescue ArgumentError
Base64.urlsafe_decode64(encoded_csrf_token)
end
end
end
end
end

View file

@ -175,12 +175,15 @@ end
# common test methods
module RequestForgeryProtectionTests
def setup
@old_urlsafe_csrf_tokens = ActionController::Base.urlsafe_csrf_tokens
ActionController::Base.urlsafe_csrf_tokens = true
@token = Base64.urlsafe_encode64("railstestrailstestrailstestrails")
@old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
end
def teardown
ActionController::Base.urlsafe_csrf_tokens = @old_urlsafe_csrf_tokens
ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
end
@ -378,12 +381,27 @@ module RequestForgeryProtectionTests
end
def test_should_allow_post_with_strict_encoded_token
session[:_csrf_token] = Base64.strict_encode64("railstestrailstestrailstestrails")
@controller.stub :form_authenticity_token, @token do
assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
token_length = (ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH * 4.0 / 3).ceil
token_including_url_unsafe_chars = "+/".ljust(token_length, "A")
session[:_csrf_token] = token_including_url_unsafe_chars
@controller.stub :form_authenticity_token, token_including_url_unsafe_chars do
assert_not_blocked { post :index, params: { custom_authenticity_token: token_including_url_unsafe_chars } }
end
end
def test_should_allow_post_with_urlsafe_token_when_migrating
config_before = ActionController::Base.urlsafe_csrf_tokens
ActionController::Base.urlsafe_csrf_tokens = false
token_length = (ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH * 4.0 / 3).ceil
token_including_url_safe_chars = "-_".ljust(token_length, "A")
session[:_csrf_token] = token_including_url_safe_chars
@controller.stub :form_authenticity_token, token_including_url_safe_chars do
assert_not_blocked { post :index, params: { custom_authenticity_token: token_including_url_safe_chars } }
end
ensure
ActionController::Base.urlsafe_csrf_tokens = config_before
end
def test_should_allow_patch_with_token
session[:_csrf_token] = @token
@controller.stub :form_authenticity_token, @token do

View file

@ -505,6 +505,8 @@ The schema dumper adds two additional configuration options:
* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController::Base`. This is false by default.
* `config.action_controller.urlsafe_csrf_tokens` configures whether generated CSRF tokens are URL-safe. Defaults to `false`.
* `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`.
* `config.action_controller.permit_all_parameters` sets all the parameters for mass assignment to be permitted by default. The default value is `false`.
@ -994,6 +996,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
- `config.active_job.skip_after_callbacks_if_terminated`: `true`
- `config.action_dispatch.cookies_same_site_protection`: `:lax`
- `ActiveSupport.utc_to_local_returns_utc_offset_times`: `true`
- `config.action_controller.urlsafe_csrf_tokens`: `true`
#### For '6.0', new defaults from previous versions below and:

View file

@ -177,6 +177,10 @@ module Rails
action_dispatch.cookies_same_site_protection = :lax
end
if respond_to?(:action_controller)
action_controller.urlsafe_csrf_tokens = true
end
ActiveSupport.utc_to_local_returns_utc_offset_times = true
else
raise "Unknown version #{target_version.to_s.inspect}"

View file

@ -25,6 +25,12 @@
# It's best enabled when your entire app is migrated and stable on 6.1.
# Rails.application.config.action_dispatch.cookies_same_site_protection = :lax
# Generate CSRF tokens that are encoded in URL-safe Base64.
#
# This change is not backwards compatible with earlier Rails versions.
# It's best enabled when your entire app is migrated and stable on 6.1.
# Rails.application.config.action_controller.urlsafe_csrf_tokens = true
# Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an
# UTC offset or a UTC time.
# ActiveSupport.utc_to_local_returns_utc_offset_times = true