diff options
4 files changed, 159 insertions, 33 deletions
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 diff --git a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt index 6c0ef31725..d83690e1b9 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. - # For APIs, you may want to use :reset_session instead. + # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index 07134cc935..06dec81d40 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -46,5 +46,87 @@ module ApplicationTests assert last_request.env["HTTP_COOKIE"] assert !last_response.headers["Set-Cookie"] end + + test "session is empty and isn't saved on unverified request when using :null_session protect method" do + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + get ':controller(/:action)' + post ':controller(/:action)' + end + RUBY + + controller :foo, <<-RUBY + class FooController < ActionController::Base + protect_from_forgery with: :null_session + + def write_session + session[:foo] = 1 + render nothing: true + end + + def read_session + render text: session[:foo].inspect + end + end + RUBY + + add_to_config <<-RUBY + config.action_controller.allow_forgery_protection = true + RUBY + + require "#{app_path}/config/environment" + + get '/foo/write_session' + get '/foo/read_session' + assert_equal '1', last_response.body + + post '/foo/read_session' # Read session using POST request without CSRF token + assert_equal 'nil', last_response.body # Stored value shouldn't be accessible + + post '/foo/write_session' # Write session using POST request without CSRF token + get '/foo/read_session' # Session shouldn't be changed + assert_equal '1', last_response.body + end + + test "cookie jar is empty and isn't saved on unverified request when using :null_session protect method" do + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + get ':controller(/:action)' + post ':controller(/:action)' + end + RUBY + + controller :foo, <<-RUBY + class FooController < ActionController::Base + protect_from_forgery with: :null_session + + def write_cookie + cookies[:foo] = '1' + render nothing: true + end + + def read_cookie + render text: cookies[:foo].inspect + end + end + RUBY + + add_to_config <<-RUBY + config.action_controller.allow_forgery_protection = true + RUBY + + require "#{app_path}/config/environment" + + get '/foo/write_cookie' + get '/foo/read_cookie' + assert_equal '"1"', last_response.body + + post '/foo/read_cookie' # Read cookie using POST request without CSRF token + assert_equal 'nil', last_response.body # Stored value shouldn't be accessible + + post '/foo/write_cookie' # Write cookie using POST request without CSRF token + get '/foo/read_cookie' # Cookie shouldn't be changed + assert_equal '"1"', last_response.body + end end end |