1894f3e
From 0fb6347bdc2ca91e012c222d92d59e90716e75ec Mon Sep 17 00:00:00 2001
1894f3e
From: Michael Koziarski <michael@koziarski.com>
1894f3e
Date: Mon, 17 Jan 2011 14:12:29 +1300
1894f3e
Subject: [PATCH 2/2] Change the CSRF whitelisting to only apply to get requests
1894f3e
1894f3e
Unfortunately the previous method of browser detection and XHR whitelisting is unable to prevent requests issued from some Flash animations and Java applets.  To ease the work required to include the CSRF token in ajax requests rails now supports providing the token in a custom http header:
1894f3e
1894f3e
 X-CSRF-Token: ...
1894f3e
1894f3e
This fixes CVE-2011-0447
1894f3e
---
1894f3e
 .../request_forgery_protection.rb                  |   15 +-
1894f3e
 actionpack/lib/action_view/helpers.rb              |    2 +
1894f3e
 actionpack/lib/action_view/helpers/csrf_helper.rb  |   14 ++
1894f3e
 .../controller/request_forgery_protection_test.rb  |  216 +++++++++-----------
1894f3e
 4 files changed, 117 insertions(+), 130 deletions(-)
1894f3e
 create mode 100644 actionpack/lib/action_view/helpers/csrf_helper.rb
1894f3e
1894f3e
diff --git a/actionpack/lib/action_controller/request_forgery_protection.rb b/actionpack/lib/action_controller/request_forgery_protection.rb
1894f3e
index 24821ff..0030857 100644
1894f3e
--- a/actionpack/lib/action_controller/request_forgery_protection.rb
1894f3e
+++ b/actionpack/lib/action_controller/request_forgery_protection.rb
1894f3e
@@ -76,7 +76,11 @@ module ActionController #:nodoc:
1894f3e
     protected
1894f3e
       # The actual before_filter that is used.  Modify this to change how you handle unverified requests.
1894f3e
       def verify_authenticity_token
1894f3e
-        verified_request? || raise(ActionController::InvalidAuthenticityToken)
1894f3e
+        verified_request? || handle_unverified_request
1894f3e
+      end
1894f3e
+
1894f3e
+      def handle_unverified_request
1894f3e
+        reset_session
1894f3e
       end
1894f3e
       
1894f3e
       # Returns true or false if a request is verified.  Checks:
1894f3e
@@ -85,11 +89,10 @@ module ActionController #:nodoc:
1894f3e
       # * is it a GET request?  Gets should be safe and idempotent
1894f3e
       # * Does the form_authenticity_token match the given token value from the params?
1894f3e
       def verified_request?
1894f3e
-        !protect_against_forgery?     ||
1894f3e
-          request.method == :get      ||
1894f3e
-          request.xhr?                ||
1894f3e
-          !verifiable_request_format? ||
1894f3e
-          form_authenticity_token == form_authenticity_param
1894f3e
+        !protect_against_forgery?                            ||
1894f3e
+          request.get?                                       ||
1894f3e
+          form_authenticity_token == form_authenticity_param ||
1894f3e
+          form_authenticity_token == request.headers['X-CSRF-Token']
1894f3e
       end
1894f3e
 
1894f3e
       def form_authenticity_param
1894f3e
diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb
1894f3e
index cea894d..debd2e7 100644
1894f3e
--- a/actionpack/lib/action_view/helpers.rb
1894f3e
+++ b/actionpack/lib/action_view/helpers.rb
1894f3e
@@ -6,6 +6,7 @@ module ActionView #:nodoc:
1894f3e
     autoload :BenchmarkHelper, 'action_view/helpers/benchmark_helper'
1894f3e
     autoload :CacheHelper, 'action_view/helpers/cache_helper'
1894f3e
     autoload :CaptureHelper, 'action_view/helpers/capture_helper'
1894f3e
+    autoload :CsrfHelper, 'action_view/helpers/csrf_helper'
1894f3e
     autoload :DateHelper, 'action_view/helpers/date_helper'
1894f3e
     autoload :DebugHelper, 'action_view/helpers/debug_helper'
1894f3e
     autoload :FormHelper, 'action_view/helpers/form_helper'
1894f3e
@@ -38,6 +39,7 @@ module ActionView #:nodoc:
1894f3e
     include BenchmarkHelper
1894f3e
     include CacheHelper
1894f3e
     include CaptureHelper
1894f3e
+    include CsrfHelper
1894f3e
     include DateHelper
1894f3e
     include DebugHelper
1894f3e
     include FormHelper
1894f3e
diff --git a/actionpack/lib/action_view/helpers/csrf_helper.rb b/actionpack/lib/action_view/helpers/csrf_helper.rb
1894f3e
new file mode 100644
1894f3e
index 0000000..e0e6c9a
1894f3e
--- /dev/null
1894f3e
+++ b/actionpack/lib/action_view/helpers/csrf_helper.rb
1894f3e
@@ -0,0 +1,14 @@
1894f3e
+module ActionView
1894f3e
+  # = Action View CSRF Helper
1894f3e
+  module Helpers
1894f3e
+    module CsrfHelper
1894f3e
+      # Returns a meta tag with the cross-site request forgery protection token
1894f3e
+      # for forms to use. Place this in your head.
1894f3e
+      def csrf_meta_tag
1894f3e
+        if protect_against_forgery?
1894f3e
+          %(<meta name="csrf-param" content="#{h(request_forgery_protection_token)}"/>\n<meta name="csrf-token" content="#{h(form_authenticity_token)}"/>)
1894f3e
+        end
1894f3e
+      end
1894f3e
+    end
1894f3e
+  end
1894f3e
+end
1894f3e
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
1894f3e
index c6ad4b9..7502905 100644
1894f3e
--- a/actionpack/test/controller/request_forgery_protection_test.rb
1894f3e
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
1894f3e
@@ -23,6 +23,10 @@ module RequestForgeryProtectionActions
1894f3e
     render :text => 'pwn'
1894f3e
   end
1894f3e
 
1894f3e
+  def meta
1894f3e
+    render :inline => "<%= csrf_meta_tag %>"
1894f3e
+  end
1894f3e
+
1894f3e
   def rescue_action(e) raise e end
1894f3e
 end
1894f3e
 
1894f3e
@@ -32,6 +36,16 @@ class RequestForgeryProtectionController < ActionController::Base
1894f3e
   protect_from_forgery :only => :index
1894f3e
 end
1894f3e
 
1894f3e
+class RequestForgeryProtectionControllerUsingOldBehaviour < ActionController::Base
1894f3e
+  include RequestForgeryProtectionActions
1894f3e
+  protect_from_forgery :only => %w(index meta)
1894f3e
+
1894f3e
+  def handle_unverified_request
1894f3e
+    raise(ActionController::InvalidAuthenticityToken)
1894f3e
+  end
1894f3e
+end
1894f3e
+
1894f3e
+
1894f3e
 class FreeCookieController < RequestForgeryProtectionController
1894f3e
   self.allow_forgery_protection = false
1894f3e
   
1894f3e
@@ -54,158 +68,92 @@ end
1894f3e
 # common test methods
1894f3e
 
1894f3e
 module RequestForgeryProtectionTests
1894f3e
-  def teardown
1894f3e
-    ActionController::Base.request_forgery_protection_token = nil
1894f3e
-  end
1894f3e
-  
1894f3e
+  def setup
1894f3e
+    @token      = "cf50faa3fe97702ca1ae"
1894f3e
 
1894f3e
-  def test_should_render_form_with_token_tag
1894f3e
-     get :index
1894f3e
-     assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
1894f3e
-   end
1894f3e
-
1894f3e
-   def test_should_render_button_to_with_token_tag
1894f3e
-     get :show_button
1894f3e
-     assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
1894f3e
-   end
1894f3e
-
1894f3e
-   def test_should_render_remote_form_with_only_one_token_parameter
1894f3e
-     get :remote_form
1894f3e
-     assert_equal 1, @response.body.scan(@token).size
1894f3e
-   end
1894f3e
-
1894f3e
-   def test_should_allow_get
1894f3e
-     get :index
1894f3e
-     assert_response :success
1894f3e
-   end
1894f3e
-
1894f3e
-   def test_should_allow_post_without_token_on_unsafe_action
1894f3e
-     post :unsafe
1894f3e
-     assert_response :success
1894f3e
-   end
1894f3e
-
1894f3e
-  def test_should_not_allow_html_post_without_token
1894f3e
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
1894f3e
-    assert_raise(ActionController::InvalidAuthenticityToken) { post :index, :format => :html }
1894f3e
+    ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
1894f3e
+    ActionController::Base.request_forgery_protection_token = :authenticity_token
1894f3e
   end
1894f3e
   
1894f3e
-  def test_should_not_allow_html_put_without_token
1894f3e
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
1894f3e
-    assert_raise(ActionController::InvalidAuthenticityToken) { put :index, :format => :html }
1894f3e
-  end
1894f3e
   
1894f3e
-  def test_should_not_allow_html_delete_without_token
1894f3e
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
1894f3e
-    assert_raise(ActionController::InvalidAuthenticityToken) { delete :index, :format => :html }
1894f3e
-  end
1894f3e
-
1894f3e
-  def test_should_allow_api_formatted_post_without_token
1894f3e
-    assert_nothing_raised do
1894f3e
-      post :index, :format => 'xml'
1894f3e
+  def test_should_render_form_with_token_tag
1894f3e
+    assert_not_blocked do
1894f3e
+      get :index
1894f3e
     end
1894f3e
+    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
1894f3e
   end
1894f3e
 
1894f3e
-  def test_should_not_allow_api_formatted_put_without_token
1894f3e
-    assert_nothing_raised do
1894f3e
-      put :index, :format => 'xml'
1894f3e
+  def test_should_render_button_to_with_token_tag
1894f3e
+    assert_not_blocked do
1894f3e
+      get :show_button
1894f3e
     end
1894f3e
+    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
1894f3e
   end
1894f3e
 
1894f3e
-  def test_should_allow_api_formatted_delete_without_token
1894f3e
-    assert_nothing_raised do
1894f3e
-      delete :index, :format => 'xml'
1894f3e
-    end
1894f3e
+  def test_should_allow_get
1894f3e
+    assert_not_blocked { get :index }
1894f3e
   end
1894f3e
 
1894f3e
-  def test_should_not_allow_api_formatted_post_sent_as_url_encoded_form_without_token
1894f3e
-    assert_raise(ActionController::InvalidAuthenticityToken) do
1894f3e
-      @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
1894f3e
-      post :index, :format => 'xml'
1894f3e
-    end
1894f3e
+  def test_should_allow_post_without_token_on_unsafe_action
1894f3e
+    assert_not_blocked { post :unsafe }
1894f3e
   end
1894f3e
 
1894f3e
-  def test_should_not_allow_api_formatted_put_sent_as_url_encoded_form_without_token
1894f3e
-    assert_raise(ActionController::InvalidAuthenticityToken) do
1894f3e
-      @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
1894f3e
-      put :index, :format => 'xml'
1894f3e
-    end
1894f3e
+  def test_should_not_allow_post_without_token
1894f3e
+    assert_blocked { post :index }
1894f3e
   end
1894f3e
 
1894f3e
-  def test_should_not_allow_api_formatted_delete_sent_as_url_encoded_form_without_token
1894f3e
-    assert_raise(ActionController::InvalidAuthenticityToken) do
1894f3e
-      @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
1894f3e
-      delete :index, :format => 'xml'
1894f3e
-    end
1894f3e
+  def test_should_not_allow_post_without_token_irrespective_of_format
1894f3e
+    assert_blocked { post :index, :format=>'xml' }
1894f3e
   end
1894f3e
 
1894f3e
-  def test_should_not_allow_api_formatted_post_sent_as_multipart_form_without_token
1894f3e
-    assert_raise(ActionController::InvalidAuthenticityToken) do
1894f3e
-      @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
1894f3e
-      post :index, :format => 'xml'
1894f3e
-    end
1894f3e
+  def test_should_not_allow_put_without_token
1894f3e
+    assert_blocked { put :index }
1894f3e
   end
1894f3e
 
1894f3e
-  def test_should_not_allow_api_formatted_put_sent_as_multipart_form_without_token
1894f3e
-    assert_raise(ActionController::InvalidAuthenticityToken) do
1894f3e
-      @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
1894f3e
-      put :index, :format => 'xml'
1894f3e
-    end
1894f3e
+  def test_should_not_allow_delete_without_token
1894f3e
+    assert_blocked { delete :index }
1894f3e
   end
1894f3e
 
1894f3e
-  def test_should_not_allow_api_formatted_delete_sent_as_multipart_form_without_token
1894f3e
-    assert_raise(ActionController::InvalidAuthenticityToken) do
1894f3e
-      @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
1894f3e
-      delete :index, :format => 'xml'
1894f3e
-    end
1894f3e
-  end
1894f3e
-  
1894f3e
-  def test_should_allow_xhr_post_without_token
1894f3e
-    assert_nothing_raised { xhr :post, :index }
1894f3e
-  end
1894f3e
-  
1894f3e
-  def test_should_allow_xhr_put_without_token
1894f3e
-    assert_nothing_raised { xhr :put, :index }
1894f3e
-  end
1894f3e
-  
1894f3e
-  def test_should_allow_xhr_delete_without_token
1894f3e
-    assert_nothing_raised { xhr :delete, :index }
1894f3e
+  def test_should_not_allow_xhr_post_without_token
1894f3e
+    assert_blocked { xhr :post, :index }
1894f3e
   end
1894f3e
-  
1894f3e
-  def test_should_allow_xhr_post_with_encoded_form_content_type_without_token
1894f3e
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
1894f3e
-    assert_nothing_raised { xhr :post, :index }
1894f3e
-  end
1894f3e
-  
1894f3e
+
1894f3e
   def test_should_allow_post_with_token
1894f3e
-    post :index, :authenticity_token => @token
1894f3e
-    assert_response :success
1894f3e
+    assert_not_blocked { post :index, :authenticity_token => @token }
1894f3e
   end
1894f3e
   
1894f3e
   def test_should_allow_put_with_token
1894f3e
-    put :index, :authenticity_token => @token
1894f3e
-    assert_response :success
1894f3e
+    assert_not_blocked { put :index, :authenticity_token => @token }
1894f3e
   end
1894f3e
   
1894f3e
   def test_should_allow_delete_with_token
1894f3e
-    delete :index, :authenticity_token => @token
1894f3e
-    assert_response :success
1894f3e
+    assert_not_blocked { delete :index, :authenticity_token => @token }
1894f3e
   end
1894f3e
   
1894f3e
-  def test_should_allow_post_with_xml
1894f3e
-    @request.env['CONTENT_TYPE'] = Mime::XML.to_s
1894f3e
-    post :index, :format => 'xml'
1894f3e
-    assert_response :success
1894f3e
+  def test_should_allow_post_with_token_in_header
1894f3e
+    @request.env['HTTP_X_CSRF_TOKEN'] = @token
1894f3e
+    assert_not_blocked { post :index }
1894f3e
+  end
1894f3e
+
1894f3e
+  def test_should_allow_delete_with_token_in_header
1894f3e
+    @request.env['HTTP_X_CSRF_TOKEN'] = @token
1894f3e
+    assert_not_blocked { delete :index }
1894f3e
   end
1894f3e
   
1894f3e
-  def test_should_allow_put_with_xml
1894f3e
-    @request.env['CONTENT_TYPE'] = Mime::XML.to_s
1894f3e
-    put :index, :format => 'xml'
1894f3e
+  def test_should_allow_put_with_token_in_header
1894f3e
+    @request.env['HTTP_X_CSRF_TOKEN'] = @token
1894f3e
+    assert_not_blocked { put :index }
1894f3e
+  end
1894f3e
+
1894f3e
+  def assert_blocked
1894f3e
+    session[:something_like_user_id] = 1
1894f3e
+    yield
1894f3e
+    assert_nil session[:something_like_user_id], "session values are still present"
1894f3e
     assert_response :success
1894f3e
   end
1894f3e
   
1894f3e
-  def test_should_allow_delete_with_xml
1894f3e
-    @request.env['CONTENT_TYPE'] = Mime::XML.to_s
1894f3e
-    delete :index, :format => 'xml'
1894f3e
+  def assert_not_blocked
1894f3e
+    assert_nothing_raised { yield }
1894f3e
     assert_response :success
1894f3e
   end
1894f3e
 end
1894f3e
@@ -214,15 +162,20 @@ end
1894f3e
 
1894f3e
 class RequestForgeryProtectionControllerTest < ActionController::TestCase
1894f3e
   include RequestForgeryProtectionTests
1894f3e
-  def setup
1894f3e
-    @controller = RequestForgeryProtectionController.new
1894f3e
-    @request    = ActionController::TestRequest.new
1894f3e
-    @request.format = :html
1894f3e
-    @response   = ActionController::TestResponse.new
1894f3e
-    @token      = "cf50faa3fe97702ca1ae"
1894f3e
 
1894f3e
-    ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
1894f3e
-    ActionController::Base.request_forgery_protection_token = :authenticity_token
1894f3e
+  test 'should emit a csrf-token meta tag' do
1894f3e
+    ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?')
1894f3e
+    get :meta
1894f3e
+    assert_equal %(<meta name="csrf-param" content="authenticity_token"/>\n<meta name="csrf-token" content="cf50faa3fe97702ca1ae<=?"/>), @response.body
1894f3e
+  end
1894f3e
+end
1894f3e
+
1894f3e
+class RequestForgeryProtectionControllerUsingOldBehaviourTest < ActionController::TestCase
1894f3e
+  include RequestForgeryProtectionTests
1894f3e
+  def assert_blocked
1894f3e
+    assert_raises(ActionController::InvalidAuthenticityToken) do
1894f3e
+      yield
1894f3e
+    end
1894f3e
   end
1894f3e
 end
1894f3e
 
1894f3e
@@ -251,15 +204,30 @@ class FreeCookieControllerTest < ActionController::TestCase
1894f3e
       assert_nothing_raised { send(method, :index)}
1894f3e
     end
1894f3e
   end
1894f3e
+
1894f3e
+  test 'should not emit a csrf-token meta tag' do
1894f3e
+    get :meta
1894f3e
+    assert @response.body.blank?, "#{@response.body.inspect} is not blank"
1894f3e
+  end
1894f3e
 end
1894f3e
 
1894f3e
+
1894f3e
+
1894f3e
+
1894f3e
+
1894f3e
 class CustomAuthenticityParamControllerTest < ActionController::TestCase
1894f3e
   def setup
1894f3e
+    ActionController::Base.request_forgery_protection_token = :custom_token_name
1894f3e
+    super
1894f3e
+  end
1894f3e
+
1894f3e
+  def teardown
1894f3e
     ActionController::Base.request_forgery_protection_token = :authenticity_token
1894f3e
+    super
1894f3e
   end
1894f3e
 
1894f3e
   def test_should_allow_custom_token
1894f3e
-    post :index, :authenticity_token => 'foobar'
1894f3e
+    post :index, :custom_token_name => 'foobar'
1894f3e
     assert_response :ok
1894f3e
   end
1894f3e
 end
1894f3e
-- 
1894f3e
1.7.2
1894f3e