Skip to content
This repository
Browse code

Change the CSRF whitelisting to only apply to get requests

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:

 X-CSRF-Token: ...

This fixes CVE-2011-0447
  • Loading branch information...
commit 66ce3843d32e9f2ac3b1da20067af53019bbb034 1 parent 354da43
Michael Koziarski authored
19  actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -89,25 +89,24 @@ def protect_from_forgery(options = {})
89 89
     end
90 90
 
91 91
     protected
92  
-
93  
-      def protect_from_forgery(options = {})
94  
-        self.request_forgery_protection_token ||= :authenticity_token
95  
-        before_filter :verify_authenticity_token, options
96  
-      end
97  
-
98 92
       # The actual before_filter that is used.  Modify this to change how you handle unverified requests.
99 93
       def verify_authenticity_token
100  
-        verified_request? || raise(ActionController::InvalidAuthenticityToken)
  94
+        verified_request? || handle_unverified_request
  95
+      end
  96
+
  97
+      def handle_unverified_request
  98
+        reset_session
101 99
       end
102 100
 
103 101
       # Returns true or false if a request is verified.  Checks:
104 102
       #
105  
-      # * is the format restricted?  By default, only HTML requests are checked.
106 103
       # * is it a GET request?  Gets should be safe and idempotent
107 104
       # * Does the form_authenticity_token match the given token value from the params?
  105
+      # * Does the X-CSRF-Token header match the form_authenticity_token
108 106
       def verified_request?
109  
-        !protect_against_forgery? || request.forgery_whitelisted? ||
110  
-          form_authenticity_token == params[request_forgery_protection_token]
  107
+        !protect_against_forgery? || request.get? ||
  108
+          form_authenticity_token == params[request_forgery_protection_token] ||
  109
+          form_authenticity_token == request.headers['X-CSRF-Token']
111 110
       end
112 111
 
113 112
       # Sets the token value for the current session.
3  actionpack/lib/action_dispatch/http/request.rb
@@ -141,8 +141,9 @@ def fullpath
141 141
     end
142 142
 
143 143
     def forgery_whitelisted?
144  
-      get? || xhr? || content_mime_type.nil? || !content_mime_type.verify_request?
  144
+      get?
145 145
     end
  146
+    deprecate :forgery_whitelisted? => "it is just an alias for 'get?' now, update your code"
146 147
 
147 148
     def media_type
148 149
       content_mime_type.to_s
197  actionpack/test/controller/request_forgery_protection_test.rb
... ...
@@ -1,5 +1,4 @@
1 1
 require 'abstract_unit'
2  
-require 'digest/sha1'
3 2
 
4 3
 # common controller actions
5 4
 module RequestForgeryProtectionActions
@@ -28,6 +27,16 @@ class RequestForgeryProtectionController < ActionController::Base
28 27
   protect_from_forgery :only => %w(index meta)
29 28
 end
30 29
 
  30
+class RequestForgeryProtectionControllerUsingOldBehaviour < ActionController::Base
  31
+  include RequestForgeryProtectionActions
  32
+  protect_from_forgery :only => %w(index meta)
  33
+
  34
+  def handle_unverified_request
  35
+    raise(ActionController::InvalidAuthenticityToken)
  36
+  end
  37
+end
  38
+
  39
+
31 40
 class FreeCookieController < RequestForgeryProtectionController
32 41
   self.allow_forgery_protection = false
33 42
 
@@ -50,153 +59,92 @@ def form_authenticity_param
50 59
 # common test methods
51 60
 
52 61
 module RequestForgeryProtectionTests
53  
-  def teardown
54  
-    ActionController::Base.request_forgery_protection_token = nil
55  
-  end
56  
-
57  
-
58  
-  def test_should_render_form_with_token_tag
59  
-     get :index
60  
-     assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
61  
-   end
62  
-
63  
-   def test_should_render_button_to_with_token_tag
64  
-     get :show_button
65  
-     assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
66  
-   end
67  
-
68  
-   def test_should_allow_get
69  
-     get :index
70  
-     assert_response :success
71  
-   end
72  
-
73  
-   def test_should_allow_post_without_token_on_unsafe_action
74  
-     post :unsafe
75  
-     assert_response :success
76  
-   end
77  
-
78  
-  def test_should_not_allow_html_post_without_token
79  
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
80  
-    assert_raise(ActionController::InvalidAuthenticityToken) { post :index, :format => :html }
81  
-  end
82  
-
83  
-  def test_should_not_allow_html_put_without_token
84  
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
85  
-    assert_raise(ActionController::InvalidAuthenticityToken) { put :index, :format => :html }
86  
-  end
87  
-
88  
-  def test_should_not_allow_html_delete_without_token
89  
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
90  
-    assert_raise(ActionController::InvalidAuthenticityToken) { delete :index, :format => :html }
91  
-  end
  62
+  def setup
  63
+    @token      = "cf50faa3fe97702ca1ae"
92 64
 
93  
-  def test_should_allow_api_formatted_post_without_token
94  
-    assert_nothing_raised do
95  
-      post :index, :format => 'xml'
96  
-    end
  65
+    ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
  66
+    ActionController::Base.request_forgery_protection_token = :authenticity_token
97 67
   end
98 68
 
99  
-  def test_should_not_allow_api_formatted_put_without_token
100  
-    assert_nothing_raised do
101  
-      put :index, :format => 'xml'
102  
-    end
103  
-  end
104 69
 
105  
-  def test_should_allow_api_formatted_delete_without_token
106  
-    assert_nothing_raised do
107  
-      delete :index, :format => 'xml'
  70
+  def test_should_render_form_with_token_tag
  71
+    assert_not_blocked do
  72
+      get :index
108 73
     end
  74
+    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
109 75
   end
110 76
 
111  
-  def test_should_not_allow_api_formatted_post_sent_as_url_encoded_form_without_token
112  
-    assert_raise(ActionController::InvalidAuthenticityToken) do
113  
-      @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
114  
-      post :index, :format => 'xml'
  77
+  def test_should_render_button_to_with_token_tag
  78
+    assert_not_blocked do
  79
+      get :show_button
115 80
     end
  81
+    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
116 82
   end
117 83
 
118  
-  def test_should_not_allow_api_formatted_put_sent_as_url_encoded_form_without_token
119  
-    assert_raise(ActionController::InvalidAuthenticityToken) do
120  
-      @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
121  
-      put :index, :format => 'xml'
122  
-    end
  84
+  def test_should_allow_get
  85
+    assert_not_blocked { get :index }
123 86
   end
124 87
 
125  
-  def test_should_not_allow_api_formatted_delete_sent_as_url_encoded_form_without_token
126  
-    assert_raise(ActionController::InvalidAuthenticityToken) do
127  
-      @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
128  
-      delete :index, :format => 'xml'
129  
-    end
  88
+  def test_should_allow_post_without_token_on_unsafe_action
  89
+    assert_not_blocked { post :unsafe }
130 90
   end
131 91
 
132  
-  def test_should_not_allow_api_formatted_post_sent_as_multipart_form_without_token
133  
-    assert_raise(ActionController::InvalidAuthenticityToken) do
134  
-      @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
135  
-      post :index, :format => 'xml'
136  
-    end
  92
+  def test_should_not_allow_post_without_token
  93
+    assert_blocked { post :index }
137 94
   end
138 95
 
139  
-  def test_should_not_allow_api_formatted_put_sent_as_multipart_form_without_token
140  
-    assert_raise(ActionController::InvalidAuthenticityToken) do
141  
-      @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
142  
-      put :index, :format => 'xml'
143  
-    end
  96
+  def test_should_not_allow_post_without_token_irrespective_of_format
  97
+    assert_blocked { post :index, :format=>'xml' }
144 98
   end
145 99
 
146  
-  def test_should_not_allow_api_formatted_delete_sent_as_multipart_form_without_token
147  
-    assert_raise(ActionController::InvalidAuthenticityToken) do
148  
-      @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
149  
-      delete :index, :format => 'xml'
150  
-    end
  100
+  def test_should_not_allow_put_without_token
  101
+    assert_blocked { put :index }
151 102
   end
152 103
 
153  
-  def test_should_allow_xhr_post_without_token
154  
-    assert_nothing_raised { xhr :post, :index }
  104
+  def test_should_not_allow_delete_without_token
  105
+    assert_blocked { delete :index }
155 106
   end
156 107
 
157  
-  def test_should_allow_xhr_put_without_token
158  
-    assert_nothing_raised { xhr :put, :index }
  108
+  def test_should_not_allow_xhr_post_without_token
  109
+    assert_blocked { xhr :post, :index }
159 110
   end
160 111
 
161  
-  def test_should_allow_xhr_delete_without_token
162  
-    assert_nothing_raised { xhr :delete, :index }
  112
+  def test_should_allow_post_with_token
  113
+    assert_not_blocked { post :index, :authenticity_token => @token }
163 114
   end
164 115
 
165  
-  def test_should_allow_xhr_post_with_encoded_form_content_type_without_token
166  
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
167  
-    assert_nothing_raised { xhr :post, :index }
  116
+  def test_should_allow_put_with_token
  117
+    assert_not_blocked { put :index, :authenticity_token => @token }
168 118
   end
169 119
 
170  
-  def test_should_allow_post_with_token
171  
-    post :index, :authenticity_token => @token
172  
-    assert_response :success
  120
+  def test_should_allow_delete_with_token
  121
+    assert_not_blocked { delete :index, :authenticity_token => @token }
173 122
   end
174 123
 
175  
-  def test_should_allow_put_with_token
176  
-    put :index, :authenticity_token => @token
177  
-    assert_response :success
  124
+  def test_should_allow_post_with_token_in_header
  125
+    @request.env['HTTP_X_CSRF_TOKEN'] = @token
  126
+    assert_not_blocked { post :index }
178 127
   end
179 128
 
180  
-  def test_should_allow_delete_with_token
181  
-    delete :index, :authenticity_token => @token
182  
-    assert_response :success
  129
+  def test_should_allow_delete_with_token_in_header
  130
+    @request.env['HTTP_X_CSRF_TOKEN'] = @token
  131
+    assert_not_blocked { delete :index }
183 132
   end
184 133
 
185  
-  def test_should_allow_post_with_xml
186  
-    @request.env['CONTENT_TYPE'] = Mime::XML.to_s
187  
-    post :index, :format => 'xml'
188  
-    assert_response :success
  134
+  def test_should_allow_put_with_token_in_header
  135
+    @request.env['HTTP_X_CSRF_TOKEN'] = @token
  136
+    assert_not_blocked { put :index }
189 137
   end
190 138
 
191  
-  def test_should_allow_put_with_xml
192  
-    @request.env['CONTENT_TYPE'] = Mime::XML.to_s
193  
-    put :index, :format => 'xml'
  139
+  def assert_blocked
  140
+    session[:something_like_user_id] = 1
  141
+    yield
  142
+    assert_nil session[:something_like_user_id], "session values are still present"
194 143
     assert_response :success
195 144
   end
196 145
 
197  
-  def test_should_allow_delete_with_xml
198  
-    @request.env['CONTENT_TYPE'] = Mime::XML.to_s
199  
-    delete :index, :format => 'xml'
  146
+  def assert_not_blocked
  147
+    assert_nothing_raised { yield }
200 148
     assert_response :success
201 149
   end
202 150
 end
@@ -205,16 +153,6 @@ def test_should_allow_delete_with_xml
205 153
 
206 154
 class RequestForgeryProtectionControllerTest < ActionController::TestCase
207 155
   include RequestForgeryProtectionTests
208  
-  def setup
209  
-    @controller = RequestForgeryProtectionController.new
210  
-    @request    = ActionController::TestRequest.new
211  
-    @request.format = :html
212  
-    @response   = ActionController::TestResponse.new
213  
-    @token      = "cf50faa3fe97702ca1ae"
214  
-
215  
-    ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
216  
-    ActionController::Base.request_forgery_protection_token = :authenticity_token
217  
-  end
218 156
 
219 157
   test 'should emit a csrf-token meta tag' do
220 158
     ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?')
@@ -223,6 +161,15 @@ def setup
223 161
   end
224 162
 end
225 163
 
  164
+class RequestForgeryProtectionControllerUsingOldBehaviourTest < ActionController::TestCase
  165
+  include RequestForgeryProtectionTests
  166
+  def assert_blocked
  167
+    assert_raises(ActionController::InvalidAuthenticityToken) do
  168
+      yield
  169
+    end
  170
+  end
  171
+end
  172
+
226 173
 class FreeCookieControllerTest < ActionController::TestCase
227 174
   def setup
228 175
     @controller = FreeCookieController.new
@@ -255,13 +202,23 @@ def test_should_allow_all_methods_without_token
255 202
   end
256 203
 end
257 204
 
  205
+
  206
+
  207
+
  208
+
258 209
 class CustomAuthenticityParamControllerTest < ActionController::TestCase
259 210
   def setup
  211
+    ActionController::Base.request_forgery_protection_token = :custom_token_name
  212
+    super
  213
+  end
  214
+
  215
+  def teardown
260 216
     ActionController::Base.request_forgery_protection_token = :authenticity_token
  217
+    super
261 218
   end
262 219
 
263 220
   def test_should_allow_custom_token
264  
-    post :index, :authenticity_token => 'foobar'
  221
+    post :index, :custom_token_name => 'foobar'
265 222
     assert_response :ok
266 223
   end
267 224
 end

0 notes on commit 66ce384

Please sign in to comment.
Something went wrong with that request. Please try again.