Package: rails / 2.3.5-1.2+squeeze8

0002-Change-the-CSRF-whitelisting-to-only-apply-to-get-re.patch Patch series | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
From 7e86f9b4d2b7dfa974c10ae7e6d8ef90f3d77f06 Mon Sep 17 00:00:00 2001
From: Michael Koziarski <michael@koziarski.com>
Date: Mon, 17 Jan 2011 14:12:29 +1300
Subject: [PATCH 2/2] 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
---
 .../request_forgery_protection.rb                  |   15 +-
 actionpack/lib/action_view/helpers.rb              |    2 +
 actionpack/lib/action_view/helpers/csrf_helper.rb  |   14 ++
 .../controller/request_forgery_protection_test.rb  |  216 +++++++++-----------
 4 files changed, 117 insertions(+), 130 deletions(-)
 create mode 100644 actionpack/lib/action_view/helpers/csrf_helper.rb

--- a/actionpack/lib/action_controller/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/request_forgery_protection.rb
@@ -76,7 +76,11 @@ module ActionController #:nodoc:
     protected
       # The actual before_filter that is used.  Modify this to change how you handle unverified requests.
       def verify_authenticity_token
-        verified_request? || raise(ActionController::InvalidAuthenticityToken)
+        verified_request? || handle_unverified_request
+      end
+
+      def handle_unverified_request
+        reset_session
       end
       
       # Returns true or false if a request is verified.  Checks:
@@ -85,11 +89,10 @@ module ActionController #:nodoc:
       # * is it a GET request?  Gets should be safe and idempotent
       # * Does the form_authenticity_token match the given token value from the params?
       def verified_request?
-        !protect_against_forgery?     ||
-          request.method == :get      ||
-          request.xhr?                ||
-          !verifiable_request_format? ||
-          form_authenticity_token == form_authenticity_param
+        !protect_against_forgery?                            ||
+          request.get?                                       ||
+          form_authenticity_token == form_authenticity_param ||
+          form_authenticity_token == request.headers['X-CSRF-Token']
       end
 
       def form_authenticity_param
--- a/actionpack/lib/action_view/helpers.rb
+++ b/actionpack/lib/action_view/helpers.rb
@@ -6,6 +6,7 @@ module ActionView #:nodoc:
     autoload :BenchmarkHelper, 'action_view/helpers/benchmark_helper'
     autoload :CacheHelper, 'action_view/helpers/cache_helper'
     autoload :CaptureHelper, 'action_view/helpers/capture_helper'
+    autoload :CsrfHelper, 'action_view/helpers/csrf_helper'
     autoload :DateHelper, 'action_view/helpers/date_helper'
     autoload :DebugHelper, 'action_view/helpers/debug_helper'
     autoload :FormHelper, 'action_view/helpers/form_helper'
@@ -38,6 +39,7 @@ module ActionView #:nodoc:
     include BenchmarkHelper
     include CacheHelper
     include CaptureHelper
+    include CsrfHelper
     include DateHelper
     include DebugHelper
     include FormHelper
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/csrf_helper.rb
@@ -0,0 +1,14 @@
+module ActionView
+  # = Action View CSRF Helper
+  module Helpers
+    module CsrfHelper
+      # Returns a meta tag with the cross-site request forgery protection token
+      # for forms to use. Place this in your head.
+      def csrf_meta_tag
+        if protect_against_forgery?
+          %(<meta name="csrf-param" content="#{h(request_forgery_protection_token)}"/>\n<meta name="csrf-token" content="#{h(form_authenticity_token)}"/>).html_safe
+        end
+      end
+    end
+  end
+end
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -23,6 +23,10 @@ module RequestForgeryProtectionActions
     render :text => 'pwn'
   end
 
+  def meta
+    render :inline => "<%= csrf_meta_tag %>"
+  end
+
   def rescue_action(e) raise e end
 end
 
@@ -32,6 +36,16 @@ class RequestForgeryProtectionController
   protect_from_forgery :only => :index
 end
 
+class RequestForgeryProtectionControllerUsingOldBehaviour < ActionController::Base
+  include RequestForgeryProtectionActions
+  protect_from_forgery :only => %w(index meta)
+
+  def handle_unverified_request
+    raise(ActionController::InvalidAuthenticityToken)
+  end
+end
+
+
 class FreeCookieController < RequestForgeryProtectionController
   self.allow_forgery_protection = false
   
@@ -54,158 +68,92 @@ end
 # common test methods
 
 module RequestForgeryProtectionTests
-  def teardown
-    ActionController::Base.request_forgery_protection_token = nil
-  end
-  
-
-  def test_should_render_form_with_token_tag
-     get :index
-     assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
-   end
-
-   def test_should_render_button_to_with_token_tag
-     get :show_button
-     assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
-   end
-
-   def test_should_render_remote_form_with_only_one_token_parameter
-     get :remote_form
-     assert_equal 1, @response.body.scan(@token).size
-   end
-
-   def test_should_allow_get
-     get :index
-     assert_response :success
-   end
-
-   def test_should_allow_post_without_token_on_unsafe_action
-     post :unsafe
-     assert_response :success
-   end
+  def setup
+    @token      = "cf50faa3fe97702ca1ae"
 
-  def test_should_not_allow_html_post_without_token
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
-    assert_raise(ActionController::InvalidAuthenticityToken) { post :index, :format => :html }
+    ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
+    ActionController::Base.request_forgery_protection_token = :authenticity_token
   end
   
-  def test_should_not_allow_html_put_without_token
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
-    assert_raise(ActionController::InvalidAuthenticityToken) { put :index, :format => :html }
-  end
   
-  def test_should_not_allow_html_delete_without_token
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
-    assert_raise(ActionController::InvalidAuthenticityToken) { delete :index, :format => :html }
-  end
-
-  def test_should_allow_api_formatted_post_without_token
-    assert_nothing_raised do
-      post :index, :format => 'xml'
+  def test_should_render_form_with_token_tag
+    assert_not_blocked do
+      get :index
     end
+    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
   end
 
-  def test_should_not_allow_api_formatted_put_without_token
-    assert_nothing_raised do
-      put :index, :format => 'xml'
+  def test_should_render_button_to_with_token_tag
+    assert_not_blocked do
+      get :show_button
     end
+    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
   end
 
-  def test_should_allow_api_formatted_delete_without_token
-    assert_nothing_raised do
-      delete :index, :format => 'xml'
-    end
+  def test_should_allow_get
+    assert_not_blocked { get :index }
   end
 
-  def test_should_not_allow_api_formatted_post_sent_as_url_encoded_form_without_token
-    assert_raise(ActionController::InvalidAuthenticityToken) do
-      @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
-      post :index, :format => 'xml'
-    end
+  def test_should_allow_post_without_token_on_unsafe_action
+    assert_not_blocked { post :unsafe }
   end
 
-  def test_should_not_allow_api_formatted_put_sent_as_url_encoded_form_without_token
-    assert_raise(ActionController::InvalidAuthenticityToken) do
-      @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
-      put :index, :format => 'xml'
-    end
+  def test_should_not_allow_post_without_token
+    assert_blocked { post :index }
   end
 
-  def test_should_not_allow_api_formatted_delete_sent_as_url_encoded_form_without_token
-    assert_raise(ActionController::InvalidAuthenticityToken) do
-      @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
-      delete :index, :format => 'xml'
-    end
+  def test_should_not_allow_post_without_token_irrespective_of_format
+    assert_blocked { post :index, :format=>'xml' }
   end
 
-  def test_should_not_allow_api_formatted_post_sent_as_multipart_form_without_token
-    assert_raise(ActionController::InvalidAuthenticityToken) do
-      @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
-      post :index, :format => 'xml'
-    end
+  def test_should_not_allow_put_without_token
+    assert_blocked { put :index }
   end
 
-  def test_should_not_allow_api_formatted_put_sent_as_multipart_form_without_token
-    assert_raise(ActionController::InvalidAuthenticityToken) do
-      @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
-      put :index, :format => 'xml'
-    end
+  def test_should_not_allow_delete_without_token
+    assert_blocked { delete :index }
   end
 
-  def test_should_not_allow_api_formatted_delete_sent_as_multipart_form_without_token
-    assert_raise(ActionController::InvalidAuthenticityToken) do
-      @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
-      delete :index, :format => 'xml'
-    end
-  end
-  
-  def test_should_allow_xhr_post_without_token
-    assert_nothing_raised { xhr :post, :index }
-  end
-  
-  def test_should_allow_xhr_put_without_token
-    assert_nothing_raised { xhr :put, :index }
-  end
-  
-  def test_should_allow_xhr_delete_without_token
-    assert_nothing_raised { xhr :delete, :index }
-  end
-  
-  def test_should_allow_xhr_post_with_encoded_form_content_type_without_token
-    @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
-    assert_nothing_raised { xhr :post, :index }
+  def test_should_not_allow_xhr_post_without_token
+    assert_blocked { xhr :post, :index }
   end
-  
+
   def test_should_allow_post_with_token
-    post :index, :authenticity_token => @token
-    assert_response :success
+    assert_not_blocked { post :index, :authenticity_token => @token }
   end
   
   def test_should_allow_put_with_token
-    put :index, :authenticity_token => @token
-    assert_response :success
+    assert_not_blocked { put :index, :authenticity_token => @token }
   end
   
   def test_should_allow_delete_with_token
-    delete :index, :authenticity_token => @token
-    assert_response :success
+    assert_not_blocked { delete :index, :authenticity_token => @token }
   end
   
-  def test_should_allow_post_with_xml
-    @request.env['CONTENT_TYPE'] = Mime::XML.to_s
-    post :index, :format => 'xml'
-    assert_response :success
+  def test_should_allow_post_with_token_in_header
+    @request.env['HTTP_X_CSRF_TOKEN'] = @token
+    assert_not_blocked { post :index }
+  end
+
+  def test_should_allow_delete_with_token_in_header
+    @request.env['HTTP_X_CSRF_TOKEN'] = @token
+    assert_not_blocked { delete :index }
   end
   
-  def test_should_allow_put_with_xml
-    @request.env['CONTENT_TYPE'] = Mime::XML.to_s
-    put :index, :format => 'xml'
+  def test_should_allow_put_with_token_in_header
+    @request.env['HTTP_X_CSRF_TOKEN'] = @token
+    assert_not_blocked { put :index }
+  end
+
+  def assert_blocked
+    session[:something_like_user_id] = 1
+    yield
+    assert_nil session[:something_like_user_id], "session values are still present"
     assert_response :success
   end
   
-  def test_should_allow_delete_with_xml
-    @request.env['CONTENT_TYPE'] = Mime::XML.to_s
-    delete :index, :format => 'xml'
+  def assert_not_blocked
+    assert_nothing_raised { yield }
     assert_response :success
   end
 end
@@ -214,15 +162,20 @@ end
 
 class RequestForgeryProtectionControllerTest < ActionController::TestCase
   include RequestForgeryProtectionTests
-  def setup
-    @controller = RequestForgeryProtectionController.new
-    @request    = ActionController::TestRequest.new
-    @request.format = :html
-    @response   = ActionController::TestResponse.new
-    @token      = "cf50faa3fe97702ca1ae"
 
-    ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
-    ActionController::Base.request_forgery_protection_token = :authenticity_token
+  test 'should emit a csrf-token meta tag' do
+    ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?')
+    get :meta
+    assert_equal %(<meta name="csrf-param" content="authenticity_token"/>\n<meta name="csrf-token" content="cf50faa3fe97702ca1ae&lt;=?"/>), @response.body
+  end
+end
+
+class RequestForgeryProtectionControllerUsingOldBehaviourTest < ActionController::TestCase
+  include RequestForgeryProtectionTests
+  def assert_blocked
+    assert_raises(ActionController::InvalidAuthenticityToken) do
+      yield
+    end
   end
 end
 
@@ -251,15 +204,30 @@ class FreeCookieControllerTest < ActionC
       assert_nothing_raised { send(method, :index)}
     end
   end
+
+  test 'should not emit a csrf-token meta tag' do
+    get :meta
+    assert_blank @response.body
+  end
 end
 
+
+
+
+
 class CustomAuthenticityParamControllerTest < ActionController::TestCase
   def setup
+    ActionController::Base.request_forgery_protection_token = :custom_token_name
+    super
+  end
+
+  def teardown
     ActionController::Base.request_forgery_protection_token = :authenticity_token
+    super
   end
 
   def test_should_allow_custom_token
-    post :index, :authenticity_token => 'foobar'
+    post :index, :custom_token_name => 'foobar'
     assert_response :ok
   end
 end