Merge pull request #7616 from lest/null-session-forgery-protection

Implement :null_session CSRF protection method
This commit is contained in:
Michael Koziarski 2012-09-13 20:44:45 -07:00
commit bb732beba7
4 changed files with 159 additions and 33 deletions

@ -49,10 +49,6 @@ module RequestForgeryProtection
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 ClassMethods
# 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 @@ def verify_authenticity_token
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

@ -56,22 +56,18 @@ def rescue_action(e) raise e end
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 @@ def show_button
end
end
class CustomAuthenticityParamController < RequestForgeryProtectionController
class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession
def form_authenticity_param
'foobar'
end
@ -268,7 +264,7 @@ def assert_not_blocked
# OK let's get our test on
class RequestForgeryProtectionControllerTest < ActionController::TestCase
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
include RequestForgeryProtectionTests
setup do

@ -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

@ -46,5 +46,87 @@ def index
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