Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
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 b5d759fd2848146f7ee7a4c1b1a4be39e2f1a2bc 1 parent b4a0d1b
Michael Koziarski NZKoz authored
20 actionpack/lib/action_controller/request_forgery_protection.rb
@@ -83,7 +83,11 @@ def protect_from_forgery(options = {})
83 83 protected
84 84 # The actual before_filter that is used. Modify this to change how you handle unverified requests.
85 85 def verify_authenticity_token
86   - verified_request? || raise(ActionController::InvalidAuthenticityToken)
  86 + verified_request? || handle_unverified_request
  87 + end
  88 +
  89 + def handle_unverified_request
  90 + reset_session
87 91 end
88 92
89 93 # Returns true or false if a request is verified. Checks:
@@ -92,12 +96,16 @@ def verify_authenticity_token
92 96 # * is it a GET request? Gets should be safe and idempotent
93 97 # * Does the form_authenticity_token match the given _token value from the params?
94 98 def verified_request?
95   - !protect_against_forgery? ||
96   - request.method == :get ||
97   - !verifiable_request_format? ||
98   - form_authenticity_token == params[request_forgery_protection_token]
  99 + !protect_against_forgery? ||
  100 + request.get? ||
  101 + form_authenticity_token == form_authenticity_param ||
  102 + form_authenticity_token == request.headers['X-CSRF-Token']
99 103 end
100   -
  104 +
  105 + def form_authenticity_param
  106 + params[request_forgery_protection_token]
  107 + end
  108 +
101 109 def verifiable_request_format?
102 110 request.content_type.nil? || request.content_type.verify_request?
103 111 end
14 actionpack/lib/action_view/helpers/csrf_helper.rb
... ... @@ -0,0 +1,14 @@
  1 +module ActionView
  2 + # = Action View CSRF Helper
  3 + module Helpers
  4 + module CsrfHelper
  5 + # Returns a meta tag with the cross-site request forgery protection token
  6 + # for forms to use. Place this in your head.
  7 + def csrf_meta_tag
  8 + if protect_against_forgery?
  9 + %(<meta name="csrf-param" content="#{h(request_forgery_protection_token)}"/>\n<meta name="csrf-token" content="#{h(form_authenticity_token)}"/>)
  10 + end
  11 + end
  12 + end
  13 + end
  14 +end
217 actionpack/test/controller/request_forgery_protection_test.rb
@@ -30,6 +30,10 @@ def unsafe
30 30 render :text => 'pwn'
31 31 end
32 32
  33 + def meta
  34 + render :inline => "<%= csrf_meta_tag %>"
  35 + end
  36 +
33 37 def rescue_action(e) raise e end
34 38 end
35 39
@@ -58,7 +62,17 @@ def rescue_action(e) raise e end
58 62 include RequestForgeryProtectionActions
59 63 end
60 64
61   -class FreeCookieController < CsrfCookieMonsterController
  65 +class RequestForgeryProtectionControllerUsingOldBehaviour < ActionController::Base
  66 + include RequestForgeryProtectionActions
  67 + protect_from_forgery :only => %w(index meta)
  68 +
  69 + def handle_unverified_request
  70 + raise(ActionController::InvalidAuthenticityToken)
  71 + end
  72 +end
  73 +
  74 +
  75 +class FreeCookieController < RequestForgeryProtectionController
62 76 self.allow_forgery_protection = false
63 77
64 78 def index
@@ -78,144 +92,109 @@ def teardown
78 92 end
79 93
80 94 def test_should_render_form_with_token_tag
81   - get :index
82   - assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
83   - end
  95 + get :index
  96 + assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
  97 + end
  98 +
  99 + def test_should_render_button_to_with_token_tag
  100 + get :show_button
  101 + assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
  102 + end
  103 +
  104 + def test_should_render_remote_form_with_only_one_token_parameter
  105 + get :remote_form
  106 + assert_equal 1, @response.body.scan(@token).size
  107 + end
  108 +
  109 + def test_should_allow_get
  110 + get :index
  111 + assert_response :success
  112 + end
  113 +
  114 + def test_should_allow_post_without_token_on_unsafe_action
  115 + post :unsafe
  116 + assert_response :success
  117 + end
  118 +
84 119
85   - def test_should_render_button_to_with_token_tag
86   - get :show_button
  120 + def test_should_render_form_with_token_tag
  121 + assert_not_blocked do
  122 + get :index
  123 + end
87 124 assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
88 125 end
89 126
90   - def test_should_render_remote_form_with_only_one_token_parameter
91   - get :remote_form
92   - assert_equal 1, @response.body.scan(@token).size
  127 + def test_should_render_button_to_with_token_tag
  128 + assert_not_blocked do
  129 + get :show_button
  130 + end
  131 + assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
93 132 end
94 133
95 134 def test_should_allow_get
96   - get :index
97   - assert_response :success
  135 + assert_not_blocked { get :index }
98 136 end
99   -
  137 +
100 138 def test_should_allow_post_without_token_on_unsafe_action
101   - post :unsafe
102   - assert_response :success
  139 + assert_not_blocked { post :unsafe }
103 140 end
104 141
105 142 def test_should_not_allow_post_without_token
106   - assert_raises(ActionController::InvalidAuthenticityToken) { post :index }
  143 + assert_blocked { post :index }
  144 + end
  145 +
  146 + def test_should_not_allow_post_without_token_irrespective_of_format
  147 + assert_blocked { post :index, :format=>'xml' }
107 148 end
108 149
109 150 def test_should_not_allow_put_without_token
110   - assert_raises(ActionController::InvalidAuthenticityToken) { put :index }
  151 + assert_blocked { put :index }
111 152 end
112 153
113 154 def test_should_not_allow_delete_without_token
114   - assert_raises(ActionController::InvalidAuthenticityToken) { delete :index }
115   - end
116   -
117   - def test_should_not_allow_api_formatted_post_without_token
118   - assert_raises(ActionController::InvalidAuthenticityToken) do
119   - post :index, :format => 'xml'
120   - end
121   - end
122   -
123   - def test_should_not_allow_api_formatted_put_without_token
124   - assert_raises(ActionController::InvalidAuthenticityToken) do
125   - put :index, :format => 'xml'
126   - end
127   - end
128   -
129   - def test_should_not_allow_api_formatted_delete_without_token
130   - assert_raises(ActionController::InvalidAuthenticityToken) do
131   - delete :index, :format => 'xml'
132   - end
133   - end
134   -
135   - def test_should_not_allow_api_formatted_post_sent_as_url_encoded_form_without_token
136   - assert_raises(ActionController::InvalidAuthenticityToken) do
137   - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
138   - post :index, :format => 'xml'
139   - end
140   - end
141   -
142   - def test_should_not_allow_api_formatted_put_sent_as_url_encoded_form_without_token
143   - assert_raises(ActionController::InvalidAuthenticityToken) do
144   - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
145   - put :index, :format => 'xml'
146   - end
147   - end
148   -
149   - def test_should_not_allow_api_formatted_delete_sent_as_url_encoded_form_without_token
150   - assert_raises(ActionController::InvalidAuthenticityToken) do
151   - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
152   - delete :index, :format => 'xml'
153   - end
154   - end
155   -
156   - def test_should_not_allow_api_formatted_post_sent_as_multipart_form_without_token
157   - assert_raises(ActionController::InvalidAuthenticityToken) do
158   - @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
159   - post :index, :format => 'xml'
160   - end
161   - end
162   -
163   - def test_should_not_allow_api_formatted_put_sent_as_multipart_form_without_token
164   - assert_raises(ActionController::InvalidAuthenticityToken) do
165   - @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
166   - put :index, :format => 'xml'
167   - end
168   - end
169   -
170   - def test_should_not_allow_api_formatted_delete_sent_as_multipart_form_without_token
171   - assert_raises(ActionController::InvalidAuthenticityToken) do
172   - @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
173   - delete :index, :format => 'xml'
174   - end
  155 + assert_blocked { delete :index }
175 156 end
176 157
177 158 def test_should_not_allow_xhr_post_without_token
178   - assert_raises(ActionController::InvalidAuthenticityToken) { xhr :post, :index }
179   - end
180   -
181   - def test_should_not_allow_xhr_put_without_token
182   - assert_raises(ActionController::InvalidAuthenticityToken) { xhr :put, :index }
183   - end
184   -
185   - def test_should_not_allow_xhr_delete_without_token
186   - assert_raises(ActionController::InvalidAuthenticityToken) { xhr :delete, :index }
  159 + assert_blocked { xhr :post, :index }
187 160 end
188   -
  161 +
189 162 def test_should_allow_post_with_token
190   - post :index, :authenticity_token => @token
191   - assert_response :success
  163 + assert_not_blocked { post :index, :authenticity_token => @token }
192 164 end
193 165
194 166 def test_should_allow_put_with_token
195   - put :index, :authenticity_token => @token
196   - assert_response :success
  167 + assert_not_blocked { put :index, :authenticity_token => @token }
197 168 end
198 169
199 170 def test_should_allow_delete_with_token
200   - delete :index, :authenticity_token => @token
201   - assert_response :success
  171 + assert_not_blocked { delete :index, :authenticity_token => @token }
202 172 end
203 173
204   - def test_should_allow_post_with_xml
205   - @request.env['CONTENT_TYPE'] = Mime::XML.to_s
206   - post :index, :format => 'xml'
207   - assert_response :success
  174 + def test_should_allow_post_with_token_in_header
  175 + @request.env['HTTP_X_CSRF_TOKEN'] = @token
  176 + assert_not_blocked { post :index }
  177 + end
  178 +
  179 + def test_should_allow_delete_with_token_in_header
  180 + @request.env['HTTP_X_CSRF_TOKEN'] = @token
  181 + assert_not_blocked { delete :index }
208 182 end
209 183
210   - def test_should_allow_put_with_xml
211   - @request.env['CONTENT_TYPE'] = Mime::XML.to_s
212   - put :index, :format => 'xml'
  184 + def test_should_allow_put_with_token_in_header
  185 + @request.env['HTTP_X_CSRF_TOKEN'] = @token
  186 + assert_not_blocked { put :index }
  187 + end
  188 +
  189 + def assert_blocked
  190 + @request.session[:something_like_user_id] = 1
  191 + yield
  192 + assert_nil @request.session[:something_like_user_id], "session values are still present"
213 193 assert_response :success
214 194 end
215 195
216   - def test_should_allow_delete_with_xml
217   - @request.env['CONTENT_TYPE'] = Mime::XML.to_s
218   - delete :index, :format => 'xml'
  196 + def assert_not_blocked
  197 + assert_nothing_raised { yield }
219 198 assert_response :success
220 199 end
221 200 end
@@ -234,6 +213,11 @@ def session_id() '123' end
234 213 @token = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('SHA1'), 'abc', '123')
235 214 ActionController::Base.request_forgery_protection_token = :authenticity_token
236 215 end
  216 +
  217 + def test_should_emit_meta_tag
  218 + get :meta
  219 + assert_equal %(<meta name="csrf-param" content="authenticity_token"/>\n<meta name="csrf-token" content="#{@token}"/>), @response.body
  220 + end
237 221 end
238 222
239 223 class RequestForgeryProtectionWithoutSecretControllerTest < Test::Unit::TestCase
@@ -248,11 +232,11 @@ def session_id() '123' end
248 232 ActionController::Base.request_forgery_protection_token = :authenticity_token
249 233 end
250 234
251   - def test_should_raise_error_without_secret
252   - assert_raises ActionController::InvalidAuthenticityToken do
253   - get :index
254   - end
255   - end
  235 + def test_should_raise_error_without_secret
  236 + assert_raises ActionController::InvalidAuthenticityToken do
  237 + get :index
  238 + end
  239 + end
256 240 end
257 241
258 242 class CsrfCookieMonsterControllerTest < Test::Unit::TestCase
@@ -294,20 +278,9 @@ def test_should_allow_all_methods_without_token
294 278 assert_nothing_raised { send(method, :index)}
295 279 end
296 280 end
297   -end
298 281
299   -class SessionOffControllerTest < Test::Unit::TestCase
300   - def setup
301   - @controller = SessionOffController.new
302   - @request = ActionController::TestRequest.new
303   - @response = ActionController::TestResponse.new
304   - @token = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('SHA1'), 'abc', '123')
305   - end
306   -
307   - def test_should_raise_correct_exception
308   - @request.session = {} # session(:off) doesn't appear to work with controller tests
309   - assert_raises(ActionController::InvalidAuthenticityToken) do
310   - post :index, :authenticity_token => @token
311   - end
  282 + def test_should_not_emit_meta_tag
  283 + get :meta
  284 + assert @response.body.blank?, "Response body should be blank"
312 285 end
313 286 end

0 comments on commit b5d759f

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