From 95be790ece75710f2588558a6d5f40fd09543b97 Mon Sep 17 00:00:00 2001 From: Sergey Nartimov Date: Thu, 13 Sep 2012 12:07:37 +0300 Subject: Implement :null_session CSRF protection method It's further work on CSRF after 245941101b1ea00a9b1af613c20b0ee994a43946. The :null_session CSRF protection method provide an empty session during request processing but doesn't reset it completely (as :reset_session does). --- .../metal/request_forgery_protection.rb | 92 ++++++++++++++++------ .../controller/request_forgery_protection_test.rb | 16 ++-- 2 files changed, 76 insertions(+), 32 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index d5f1cbc1a8..81188ac8cc 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -49,10 +49,6 @@ module ActionController #:nodoc: config_accessor :request_forgery_protection_token self.request_forgery_protection_token ||= :authenticity_token - # Controls how unverified request will be handled - config_accessor :request_forgery_protection_method - self.request_forgery_protection_method ||= :reset_session - # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode. config_accessor :allow_forgery_protection self.allow_forgery_protection = true if allow_forgery_protection.nil? @@ -78,12 +74,80 @@ module ActionController #:nodoc: # Valid Options: # # * :only/:except - Passed to the before_filter call. Set which actions are verified. - # * :with - Set the method to handle unverified request. Valid values: :exception and :reset_session (default). + # * :with - Set the method to handle unverified request. + # + # Valid unverified request handling methods are: + # * :exception - Raises ActionController::InvalidAuthenticityToken exception. + # * :reset_session - Resets the session. + # * :null_session - Provides an empty session during request but doesn't reset it completely. Used as default if :with option is not specified. def protect_from_forgery(options = {}) + include protection_method_module(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token - self.request_forgery_protection_method = options.delete(:with) if options.key?(:with) prepend_before_filter :verify_authenticity_token, options end + + private + + def protection_method_module(name) + ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify) + rescue NameError + raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session' + end + end + + module ProtectionMethods + module NullSession + protected + + # This is the method that defines the application behavior when a request is found to be unverified. + def handle_unverified_request + request.session = NullSessionHash.new + request.env['action_dispatch.request.flash_hash'] = nil + request.env['rack.session.options'] = { skip: true } + request.env['action_dispatch.cookies'] = NullCookieJar.build(request) + end + + class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: + def initialize + super(nil, nil) + @loaded = true + end + + def exists? + true + end + end + + class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: + def self.build(request) + secret = request.env[ActionDispatch::Cookies::TOKEN_KEY] + host = request.host + secure = request.ssl? + + new(secret, host, secure) + end + + def write(*) + # nothing + end + end + end + + module ResetSession + protected + + def handle_unverified_request + reset_session + end + end + + module Exception + protected + + def handle_unverified_request + raise ActionController::InvalidAuthenticityToken + end + end end protected @@ -95,22 +159,6 @@ module ActionController #:nodoc: end end - # This is the method that defines the application behavior when a request is found to be unverified. - # By default, \Rails uses request_forgery_protection_method when it finds an unverified request: - # - # * :reset_session - Resets the session. - # * :exception: - Raises ActionController::InvalidAuthenticityToken exception. - def handle_unverified_request - case request_forgery_protection_method - when :exception - raise ActionController::InvalidAuthenticityToken - when :reset_session - reset_session - else - raise ArgumentError, 'Invalid request forgery protection method, use :exception or :reset_session' - end - end - # Returns true or false if a request is verified. Checks: # # * is it a GET request? Gets should be safe and idempotent diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 0289f4070b..1f637eb791 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -56,22 +56,18 @@ module RequestForgeryProtectionActions end # sample controllers -class RequestForgeryProtectionController < ActionController::Base +class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta) + protect_from_forgery :only => %w(index meta), :with => :reset_session end class RequestForgeryProtectionControllerUsingException < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta) - - def handle_unverified_request - raise(ActionController::InvalidAuthenticityToken) - end + protect_from_forgery :only => %w(index meta), :with => :exception end -class FreeCookieController < RequestForgeryProtectionController +class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession self.allow_forgery_protection = false def index @@ -83,7 +79,7 @@ class FreeCookieController < RequestForgeryProtectionController end end -class CustomAuthenticityParamController < RequestForgeryProtectionController +class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession def form_authenticity_param 'foobar' end @@ -268,7 +264,7 @@ end # OK let's get our test on -class RequestForgeryProtectionControllerTest < ActionController::TestCase +class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase include RequestForgeryProtectionTests setup do -- cgit v1.2.3