From 95be790ece75710f2588558a6d5f40fd09543b97 Mon Sep 17 00:00:00 2001
From: Sergey Nartimov <just.lest@gmail.com>
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:
       #
       # * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified.
-      # * <tt>:with</tt> - Set the method to handle unverified request. Valid values: <tt>:exception</tt> and <tt>:reset_session</tt> (default).
+      # * <tt>:with</tt> - Set the method to handle unverified request.
+      #
+      # Valid unverified request handling methods are:
+      # * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
+      # * <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 = {})
+        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 <tt>request_forgery_protection_method</tt> when it finds an unverified request:
-      #
-      # * <tt>:reset_session</tt> - Resets the session.
-      # * <tt>:exception</tt>: - 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