request_forgery_protection_test.rb 14.6 KB
Newer Older
1
require 'abstract_unit'
M
Michael Koziarski 已提交
2
require 'digest/sha1'
3
require "active_support/log_subscriber/test_helper"
4

5 6 7 8 9
# common controller actions
module RequestForgeryProtectionActions
  def index
    render :inline => "<%= form_tag('/') {} %>"
  end
10

11
  def show_button
12
    render :inline => "<%= button_to('New', '/') %>"
13
  end
14

15 16 17 18 19 20 21 22
  def external_form
    render :inline => "<%= form_tag('http://farfar.away/form', :authenticity_token => 'external_token') {} %>"
  end

  def external_form_without_protection
    render :inline => "<%= form_tag('http://farfar.away/form', :authenticity_token => false) {} %>"
  end

23 24 25
  def unsafe
    render :text => 'pwn'
  end
26

J
Jeremy Kemper 已提交
27
  def meta
28
    render :inline => "<%= csrf_meta_tags %>"
J
Jeremy Kemper 已提交
29 30
  end

31
  def external_form_for
32
    render :inline => "<%= form_for(:some_resource, :authenticity_token => 'external_token') {} %>"
33 34 35
  end

  def form_for_without_protection
36
    render :inline => "<%= form_for(:some_resource, :authenticity_token => false ) {} %>"
37
  end
38 39 40 41 42

  def form_for_remote
    render :inline => "<%= form_for(:some_resource, :remote => true ) {} %>"
  end

43 44 45 46
  def form_for_remote_with_token
    render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => true ) {} %>"
  end

47 48 49 50 51 52 53 54
  def form_for_with_token
    render :inline => "<%= form_for(:some_resource, :authenticity_token => true ) {} %>"
  end

  def form_for_remote_with_external_token
    render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>"
  end

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
  def same_origin_js
    render js: 'foo();'
  end

  def negotiate_same_origin
    respond_to do |format|
      format.js { same_origin_js }
    end
  end

  def cross_origin_js
    same_origin_js
  end

  def negotiate_cross_origin
    negotiate_same_origin
  end

73
  def rescue_action(e) raise e end
74 75 76
end

# sample controllers
77
class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base
78
  include RequestForgeryProtectionActions
79
  protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :reset_session
80 81
end

82
class RequestForgeryProtectionControllerUsingException < ActionController::Base
83
  include RequestForgeryProtectionActions
84
  protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :exception
85 86
end

87 88 89 90 91 92 93 94 95 96 97 98
class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
  protect_from_forgery :with => :null_session

  def signed
    cookies.signed[:foo] = 'bar'
    render :nothing => true
  end

  def encrypted
    cookies.encrypted[:foo] = 'bar'
    render :nothing => true
  end
99 100 101 102 103

  def try_to_reset_session
    reset_session
    render :nothing => true
  end
104
end
105

106
class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
107
  self.allow_forgery_protection = false
108

109 110 111
  def index
    render :inline => "<%= form_tag('/') {} %>"
  end
112

113
  def show_button
114
    render :inline => "<%= button_to('New', '/') %>"
115 116 117
  end
end

118
class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession
119 120 121 122 123
  def form_authenticity_param
    'foobar'
  end
end

124
# common test methods
125
module RequestForgeryProtectionTests
126 127
  def setup
    @token      = "cf50faa3fe97702ca1ae"
128

129
    SecureRandom.stubs(:base64).returns(@token)
Z
Zuhao Wan 已提交
130
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
131
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
E
Emilio Tagua 已提交
132 133
  end

134
  def teardown
Z
Zuhao Wan 已提交
135
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
136
  end
137

138 139 140 141
  def test_should_render_form_with_token_tag
    assert_not_blocked do
      get :index
    end
142
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
143 144
  end

E
Emilio Tagua 已提交
145
  def test_should_render_button_to_with_token_tag
146 147 148
    assert_not_blocked do
      get :show_button
    end
149
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
E
Emilio Tagua 已提交
150 151
  end

152
  def test_should_render_form_without_token_tag_if_remote
153 154 155
    assert_not_blocked do
      get :form_for_remote
    end
156
    assert_no_match(/authenticity_token/, response.body)
157 158
  end

159 160
  def test_should_render_form_with_token_tag_if_remote_and_embedding_token_is_on
    original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
161
    begin
162
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
163 164 165
      assert_not_blocked do
        get :form_for_remote
      end
166
      assert_match(/authenticity_token/, response.body)
167
    ensure
168
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
169 170 171
    end
  end

172 173 174 175
  def test_should_render_form_with_token_tag_if_remote_and_external_authenticity_token_requested_and_embedding_is_on
    original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
    begin
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
176
      assert_not_blocked do
177
        get :form_for_remote_with_external_token
178
      end
179
      assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
180
    ensure
181
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
182
    end
183 184
  end

185 186 187 188
  def test_should_render_form_with_token_tag_if_remote_and_external_authenticity_token_requested
    assert_not_blocked do
      get :form_for_remote_with_external_token
    end
189
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
190 191
  end

192 193 194 195
  def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested
    assert_not_blocked do
      get :form_for_remote_with_token
    end
196
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
197 198
  end

199 200 201 202
  def test_should_render_form_with_token_tag_with_authenticity_token_requested
    assert_not_blocked do
      get :form_for_with_token
    end
203
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
204 205
  end

E
Emilio Tagua 已提交
206
  def test_should_allow_get
207
    assert_not_blocked { get :index }
E
Emilio Tagua 已提交
208 209
  end

210 211 212 213
  def test_should_allow_head
    assert_not_blocked { head :index }
  end

E
Emilio Tagua 已提交
214
  def test_should_allow_post_without_token_on_unsafe_action
215
    assert_not_blocked { post :unsafe }
216 217
  end

218 219
  def test_should_not_allow_post_without_token
    assert_blocked { post :index }
220 221
  end

222
  def test_should_not_allow_post_without_token_irrespective_of_format
223
    assert_blocked { post :index, format: 'xml' }
224 225
  end

226 227 228 229
  def test_should_not_allow_patch_without_token
    assert_blocked { patch :index }
  end

230 231
  def test_should_not_allow_put_without_token
    assert_blocked { put :index }
232
  end
233

234 235
  def test_should_not_allow_delete_without_token
    assert_blocked { delete :index }
236
  end
237

238 239
  def test_should_not_allow_xhr_post_without_token
    assert_blocked { xhr :post, :index }
240
  end
241

242
  def test_should_allow_post_with_token
243
    assert_not_blocked { post :index, :custom_authenticity_token => @token }
244
  end
245

246 247 248 249
  def test_should_allow_patch_with_token
    assert_not_blocked { patch :index, :custom_authenticity_token => @token }
  end

250
  def test_should_allow_put_with_token
251
    assert_not_blocked { put :index, :custom_authenticity_token => @token }
252
  end
253

254
  def test_should_allow_delete_with_token
255
    assert_not_blocked { delete :index, :custom_authenticity_token => @token }
256
  end
257

258 259 260
  def test_should_allow_post_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { post :index }
261
  end
262

263 264 265
  def test_should_allow_delete_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { delete :index }
266
  end
267

268 269 270 271 272
  def test_should_allow_patch_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { patch :index }
  end

273 274 275
  def test_should_allow_put_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { put :index }
276
  end
277

278 279 280 281 282 283 284 285 286 287
  def test_should_warn_on_missing_csrf_token
    old_logger = ActionController::Base.logger
    logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
    ActionController::Base.logger = logger

    begin
      assert_blocked { post :index }

      assert_equal 1, logger.logged(:warn).size
      assert_match(/CSRF token authenticity/, logger.logged(:warn).last)
M
Mike Dillon 已提交
288
    ensure
289 290 291
      ActionController::Base.logger = old_logger
    end
  end
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307

  def test_should_not_warn_if_csrf_logging_disabled
    old_logger = ActionController::Base.logger
    logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
    ActionController::Base.logger = logger
    ActionController::Base.log_warning_on_csrf_failure = false

    begin
      assert_blocked { post :index }

      assert_equal 0, logger.logged(:warn).size
    ensure
      ActionController::Base.logger = old_logger
      ActionController::Base.log_warning_on_csrf_failure = true
    end
  end
308

309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
  def test_should_only_allow_same_origin_js_get_with_xhr_header
    assert_cross_origin_blocked { get :same_origin_js }
    assert_cross_origin_blocked { get :same_origin_js, format: 'js' }
    assert_cross_origin_blocked do
      @request.accept = 'text/javascript'
      get :negotiate_same_origin
    end

    assert_cross_origin_not_blocked { xhr :get, :same_origin_js }
    assert_cross_origin_not_blocked { xhr :get, :same_origin_js, format: 'js' }
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
      xhr :get, :negotiate_same_origin
    end
  end

325 326 327 328 329 330 331 332 333 334
  # Allow non-GET requests since GET is all a remote <script> tag can muster.
  def test_should_allow_non_get_js_without_xhr_header
    assert_cross_origin_not_blocked { post :same_origin_js, custom_authenticity_token: @token }
    assert_cross_origin_not_blocked { post :same_origin_js, format: 'js', custom_authenticity_token: @token }
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
      post :negotiate_same_origin, custom_authenticity_token: @token
    end
  end

335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
  def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled
    assert_cross_origin_not_blocked { get :cross_origin_js }
    assert_cross_origin_not_blocked { get :cross_origin_js, format: 'js' }
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
      get :negotiate_cross_origin
    end

    assert_cross_origin_not_blocked { xhr :get, :cross_origin_js }
    assert_cross_origin_not_blocked { xhr :get, :cross_origin_js, format: 'js' }
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
      xhr :get, :negotiate_cross_origin
    end
  end

351 352 353 354
  def assert_blocked
    session[:something_like_user_id] = 1
    yield
    assert_nil session[:something_like_user_id], "session values are still present"
355 356
    assert_response :success
  end
357

358 359
  def assert_not_blocked
    assert_nothing_raised { yield }
360 361
    assert_response :success
  end
362 363 364 365 366 367 368 369 370 371

  def assert_cross_origin_blocked
    assert_raises(ActionController::InvalidCrossOriginRequest) do
      yield
    end
  end

  def assert_cross_origin_not_blocked
    assert_not_blocked { yield }
  end
372 373
end

374
# OK let's get our test on
375

376
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
377
  include RequestForgeryProtectionTests
J
Jeremy Kemper 已提交
378

379
  setup do
Z
Zuhao Wan 已提交
380
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
381 382 383 384
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
  end

  teardown do
Z
Zuhao Wan 已提交
385
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
386 387
  end

388
  test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
389
    SecureRandom.stubs(:base64).returns(@token + '<=?')
J
Jeremy Kemper 已提交
390
    get :meta
391
    assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
392
    assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae&lt;=?'
J
Jeremy Kemper 已提交
393
  end
394 395
end

396 397 398 399 400
class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
  class NullSessionDummyKeyGenerator
    def generate_key(secret)
      '03312270731a2ed0d11ed091c2338a06'
    end
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
  end

  def setup
    @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new
  end

  test 'should allow to set signed cookies' do
    post :signed
    assert_response :ok
  end

  test 'should allow to set encrypted cookies' do
    post :encrypted
    assert_response :ok
  end
416 417 418 419 420

  test 'should allow reset_session' do
    post :try_to_reset_session
    assert_response :ok
  end
421 422
end

423
class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
424 425 426 427 428 429 430 431
  include RequestForgeryProtectionTests
  def assert_blocked
    assert_raises(ActionController::InvalidAuthenticityToken) do
      yield
    end
  end
end

432
class FreeCookieControllerTest < ActionController::TestCase
433 434 435 436
  def setup
    @controller = FreeCookieController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
J
Jeremy Kemper 已提交
437
    @token      = "cf50faa3fe97702ca1ae"
438

439
    SecureRandom.stubs(:base64).returns(@token)
440
  end
441

442 443 444 445
  def test_should_not_render_form_with_token_tag
    get :index
    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
  end
446

447 448 449 450
  def test_should_not_render_button_to_with_token_tag
    get :show_button
    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
  end
451

452
  def test_should_allow_all_methods_without_token
453
    [:post, :patch, :put, :delete].each do |method|
454 455 456
      assert_nothing_raised { send(method, :index)}
    end
  end
J
Jeremy Kemper 已提交
457 458 459

  test 'should not emit a csrf-token meta tag' do
    get :meta
460
    assert @response.body.blank?
J
Jeremy Kemper 已提交
461
  end
462
end
463 464 465

class CustomAuthenticityParamControllerTest < ActionController::TestCase
  def setup
466
    super
467 468 469
    @old_logger = ActionController::Base.logger
    @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
    @token = "foobar"
Z
Zuhao Wan 已提交
470
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
471
    ActionController::Base.request_forgery_protection_token = @token
472 473 474
  end

  def teardown
Z
Zuhao Wan 已提交
475
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
476
    super
477 478
  end

479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
  def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
    ActionController::Base.logger = @logger
    SecureRandom.stubs(:base64).returns(@token)

    begin
      post :index, :custom_token_name => 'foobar'
      assert_equal 0, @logger.logged(:warn).size
    ensure
      ActionController::Base.logger = @old_logger
    end
  end

  def test_should_warn_if_form_authenticity_param_does_not_match_form_authenticity_token
    ActionController::Base.logger = @logger

    begin
      post :index, :custom_token_name => 'bazqux'
      assert_equal 1, @logger.logged(:warn).size
    ensure
      ActionController::Base.logger = @old_logger
    end
500 501
  end
end