mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #7616 from lest/null-session-forgery-protection
Implement :null_session CSRF protection method
This commit is contained in:
commit
bb732beba7
4 changed files with 159 additions and 33 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 @@ 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
|
||||
|
|
Loading…
Reference in a new issue