aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/metal/request_forgery_protection.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller/metal/request_forgery_protection.rb')
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb113
1 files changed, 92 insertions, 21 deletions
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 64f6f7cf51..b2f0b382b9 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -77,6 +77,14 @@ module ActionController #:nodoc:
config_accessor :log_warning_on_csrf_failure
self.log_warning_on_csrf_failure = true
+ # Controls whether the Origin header is checked in addition to the CSRF token.
+ config_accessor :forgery_protection_origin_check
+ self.forgery_protection_origin_check = false
+
+ # Controls whether form-action/method specific CSRF tokens are used.
+ config_accessor :per_form_csrf_tokens
+ self.per_form_csrf_tokens = false
+
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
@@ -98,13 +106,13 @@ module ActionController #:nodoc:
#
# Valid Options:
#
- # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <tt>only: [ :create, :create_all ]</tt>.
+ # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
# * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
- # * <tt>:prepend</tt> - By default, the verification of the authentication token is added to the front of the
- # callback chain. If you need to make the verification depend on other callbacks, like authentication methods
- # (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the
- # verification callback in the position of the protect_from_forgery call. This means any callbacks added
- # before are run first.
+ # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
+ # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
+ # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
+ #
+ # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
# * <tt>:with</tt> - Set the method to handle unverified request.
#
# Valid unverified request handling methods are:
@@ -112,7 +120,7 @@ module ActionController #:nodoc:
# * <tt>:reset_session</tt> - Resets the session.
# * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
def protect_from_forgery(options = {})
- options = options.reverse_merge(prepend: true)
+ options = options.reverse_merge(prepend: false)
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
self.request_forgery_protection_token ||= :authenticity_token
@@ -257,21 +265,41 @@ module ActionController #:nodoc:
# * Does the X-CSRF-Token header match the form_authenticity_token
def verified_request?
!protect_against_forgery? || request.get? || request.head? ||
- valid_authenticity_token?(session, form_authenticity_param) ||
- valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
+ (valid_request_origin? && any_authenticity_token_valid?)
+ end
+
+ # Checks if any of the authenticity tokens from the request are valid.
+ def any_authenticity_token_valid?
+ request_authenticity_tokens.any? do |token|
+ valid_authenticity_token?(session, token)
+ end
+ end
+
+ # Possible authenticity tokens sent in the request.
+ def request_authenticity_tokens
+ [form_authenticity_param, request.x_csrf_token]
end
# Sets the token value for the current session.
- def form_authenticity_token
- masked_authenticity_token(session)
+ def form_authenticity_token(form_options: {})
+ masked_authenticity_token(session, form_options: form_options)
end
# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
- def masked_authenticity_token(session)
+ def masked_authenticity_token(session, form_options: {})
+ action, method = form_options.values_at(:action, :method)
+
+ raw_token = if per_form_csrf_tokens && action && method
+ action_path = normalize_action_path(action)
+ per_form_csrf_token(session, action_path, method)
+ else
+ real_csrf_token(session)
+ end
+
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
- encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
masked_token = one_time_pad + encrypted_csrf_token
Base64.strict_encode64(masked_token)
end
@@ -301,30 +329,58 @@ module ActionController #:nodoc:
compare_with_real_token masked_token, session
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
- # Split the token into the one-time pad and the encrypted
- # value and decrypt it
- one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
- encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
- csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
-
- compare_with_real_token csrf_token, session
+ csrf_token = unmask_token(masked_token)
+ compare_with_real_token(csrf_token, session) ||
+ valid_per_form_csrf_token?(csrf_token, session)
else
false # Token is malformed
end
end
+ def unmask_token(masked_token)
+ # Split the token into the one-time pad and the encrypted
+ # value and decrypt it
+ one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
+ encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
+ xor_byte_strings(one_time_pad, encrypted_csrf_token)
+ end
+
def compare_with_real_token(token, session)
ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
end
+ def valid_per_form_csrf_token?(token, session)
+ if per_form_csrf_tokens
+ correct_token = per_form_csrf_token(
+ session,
+ normalize_action_path(request.fullpath),
+ request.request_method
+ )
+
+ ActiveSupport::SecurityUtils.secure_compare(token, correct_token)
+ else
+ false
+ end
+ end
+
def real_csrf_token(session)
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
Base64.strict_decode64(session[:_csrf_token])
end
+ def per_form_csrf_token(session, action_path, method)
+ OpenSSL::HMAC.digest(
+ OpenSSL::Digest::SHA256.new,
+ real_csrf_token(session),
+ [action_path, method.downcase].join("#")
+ )
+ end
+
def xor_byte_strings(s1, s2)
- s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
+ s2_bytes = s2.bytes
+ s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
+ s2_bytes.pack('C*')
end
# The form's authenticity parameter. Override to provide your own.
@@ -336,5 +392,20 @@ module ActionController #:nodoc:
def protect_against_forgery?
allow_forgery_protection
end
+
+ # Checks if the request originated from the same origin by looking at the
+ # Origin header.
+ def valid_request_origin?
+ if forgery_protection_origin_check
+ # We accept blank origin headers because some user agents don't send it.
+ request.origin.nil? || request.origin == request.base_url
+ else
+ true
+ end
+ end
+
+ def normalize_action_path(action_path)
+ action_path.split('?').first.to_s.chomp('/')
+ end
end
end