request_forgery_protection_test.rb 24.3 KB
Newer Older
1
require 'abstract_unit'
2
require "active_support/log_subscriber/test_helper"
3

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

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

14
  def unsafe
15
    render plain: 'pwn'
16
  end
17

J
Jeremy Kemper 已提交
18
  def meta
19
    render :inline => "<%= csrf_meta_tags %>"
J
Jeremy Kemper 已提交
20 21
  end

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

26 27 28 29
  def form_for_remote_with_token
    render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => true ) {} %>"
  end

30 31 32 33 34 35 36 37
  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

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
  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

56 57 58
end

# sample controllers
59
class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base
60
  include RequestForgeryProtectionActions
61
  protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :reset_session
62 63
end

64
class RequestForgeryProtectionControllerUsingException < ActionController::Base
65
  include RequestForgeryProtectionActions
66
  protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :exception
67 68
end

69 70 71 72 73
class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
  protect_from_forgery :with => :null_session

  def signed
    cookies.signed[:foo] = 'bar'
74
    head :ok
75 76 77 78
  end

  def encrypted
    cookies.encrypted[:foo] = 'bar'
79
    head :ok
80
  end
81 82 83

  def try_to_reset_session
    reset_session
84
    head :ok
85
  end
86
end
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
class PrependProtectForgeryBaseController < ActionController::Base
  before_action :custom_action
  attr_accessor :called_callbacks

  def index
    render inline: 'OK'
  end

  protected

  def add_called_callback(name)
    @called_callbacks ||= []
    @called_callbacks << name
  end


  def custom_action
    add_called_callback("custom_action")
  end

  def verify_authenticity_token
    add_called_callback("verify_authenticity_token")
  end
end

113
class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
114
  self.allow_forgery_protection = false
115

116 117 118
  def index
    render :inline => "<%= form_tag('/') {} %>"
  end
119

120
  def show_button
121
    render :inline => "<%= button_to('New', '/') %>"
122 123 124
  end
end

125
class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession
126 127 128 129 130
  def form_authenticity_param
    'foobar'
  end
end

B
Ben Toews 已提交
131 132 133 134 135 136 137 138
class PerFormTokensController < ActionController::Base
  protect_from_forgery :with => :exception
  self.per_form_csrf_tokens = true

  def index
    render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: (params[:form_method] || :post) %>"
  end

139 140 141 142
  def button_to
    render inline: "<%= button_to 'Button', (params[:form_path] || '/per_form_tokens/post_one'), method: (params[:form_method] || :post) %>"
  end

B
Ben Toews 已提交
143 144 145 146 147 148 149 150 151
  def post_one
    render plain: ''
  end

  def post_two
    render plain: ''
  end
end

152
# common test methods
153
module RequestForgeryProtectionTests
154
  def setup
155
    @token = Base64.strict_encode64('railstestrailstestrailstestrails')
Z
Zuhao Wan 已提交
156
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
157
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
E
Emilio Tagua 已提交
158 159
  end

160
  def teardown
Z
Zuhao Wan 已提交
161
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
162
  end
163

164
  def test_should_render_form_with_token_tag
165 166 167 168 169
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :index
      end
      assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
170
    end
171 172
  end

E
Emilio Tagua 已提交
173
  def test_should_render_button_to_with_token_tag
174 175 176 177 178
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :show_button
      end
      assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
179
    end
E
Emilio Tagua 已提交
180 181
  end

182
  def test_should_render_form_without_token_tag_if_remote
183 184 185
    assert_not_blocked do
      get :form_for_remote
    end
186
    assert_no_match(/authenticity_token/, response.body)
187 188
  end

189 190
  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
191
    begin
192
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
193 194 195
      assert_not_blocked do
        get :form_for_remote
      end
196
      assert_match(/authenticity_token/, response.body)
197
    ensure
198
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
199 200 201
    end
  end

202 203 204 205
  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
206
      assert_not_blocked do
207
        get :form_for_remote_with_external_token
208
      end
209
      assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
210
    ensure
211
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
212
    end
213 214
  end

215 216 217 218
  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
219
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
220 221
  end

222
  def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested
223 224 225 226 227
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :form_for_remote_with_token
      end
      assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
228 229 230
    end
  end

231
  def test_should_render_form_with_token_tag_with_authenticity_token_requested
232 233 234 235 236
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :form_for_with_token
      end
      assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
237 238 239
    end
  end

E
Emilio Tagua 已提交
240
  def test_should_allow_get
241
    assert_not_blocked { get :index }
E
Emilio Tagua 已提交
242 243
  end

244 245 246 247
  def test_should_allow_head
    assert_not_blocked { head :index }
  end

E
Emilio Tagua 已提交
248
  def test_should_allow_post_without_token_on_unsafe_action
249
    assert_not_blocked { post :unsafe }
250 251
  end

252 253
  def test_should_not_allow_post_without_token
    assert_blocked { post :index }
254 255
  end

256
  def test_should_not_allow_post_without_token_irrespective_of_format
257
    assert_blocked { post :index, format: 'xml' }
258 259
  end

260 261 262 263
  def test_should_not_allow_patch_without_token
    assert_blocked { patch :index }
  end

264 265
  def test_should_not_allow_put_without_token
    assert_blocked { put :index }
266
  end
267

268 269
  def test_should_not_allow_delete_without_token
    assert_blocked { delete :index }
270
  end
271

272
  def test_should_not_allow_xhr_post_without_token
273
    assert_blocked { post :index, xhr: true }
274
  end
275

276
  def test_should_allow_post_with_token
277 278 279 280
    session[:_csrf_token] = @token
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
    end
281
  end
282

283
  def test_should_allow_patch_with_token
284 285 286 287
    session[:_csrf_token] = @token
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } }
    end
288 289
  end

290
  def test_should_allow_put_with_token
291 292 293 294
    session[:_csrf_token] = @token
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked { put :index, params: { custom_authenticity_token: @token } }
    end
295
  end
296

297
  def test_should_allow_delete_with_token
298 299 300 301
    session[:_csrf_token] = @token
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } }
    end
302
  end
303

304
  def test_should_allow_post_with_token_in_header
305
    session[:_csrf_token] = @token
306 307
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { post :index }
308
  end
309

310
  def test_should_allow_delete_with_token_in_header
311
    session[:_csrf_token] = @token
312 313
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { delete :index }
314
  end
315

316
  def test_should_allow_patch_with_token_in_header
317
    session[:_csrf_token] = @token
318 319 320 321
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { patch :index }
  end

322
  def test_should_allow_put_with_token_in_header
323
    session[:_csrf_token] = @token
324 325
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { put :index }
326
  end
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
  def test_should_allow_post_with_origin_checking_and_correct_origin
    forgery_protection_origin_check do
      session[:_csrf_token] = @token
      @controller.stub :form_authenticity_token, @token do
        assert_not_blocked do
          @request.set_header 'HTTP_ORIGIN', 'http://test.host'
          post :index, params: { custom_authenticity_token: @token }
        end
      end
    end
  end

  def test_should_allow_post_with_origin_checking_and_no_origin
    forgery_protection_origin_check do
      session[:_csrf_token] = @token
      @controller.stub :form_authenticity_token, @token do
        assert_not_blocked do
          post :index, params: { custom_authenticity_token: @token }
        end
      end
    end
  end

  def test_should_block_post_with_origin_checking_and_wrong_origin
    forgery_protection_origin_check do
      session[:_csrf_token] = @token
      @controller.stub :form_authenticity_token, @token do
        assert_blocked do
          @request.set_header 'HTTP_ORIGIN', 'http://bad.host'
          post :index, params: { custom_authenticity_token: @token }
        end
      end
    end
  end

363 364 365 366 367 368 369 370 371 372
  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 已提交
373
    ensure
374 375 376
      ActionController::Base.logger = old_logger
    end
  end
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392

  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
393

394 395 396 397 398 399 400 401
  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

402 403
    assert_cross_origin_not_blocked { get :same_origin_js, xhr: true }
    assert_cross_origin_not_blocked { get :same_origin_js, xhr: true, format: 'js'}
404 405
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
406
      get :negotiate_same_origin, xhr: true
407 408 409
    end
  end

410 411
  # Allow non-GET requests since GET is all a remote <script> tag can muster.
  def test_should_allow_non_get_js_without_xhr_header
412
    session[:_csrf_token] = @token
413 414
    assert_cross_origin_not_blocked { post :same_origin_js, params: { custom_authenticity_token: @token } }
    assert_cross_origin_not_blocked { post :same_origin_js, params: { format: 'js', custom_authenticity_token: @token } }
415 416
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
417
      post :negotiate_same_origin, params: { custom_authenticity_token: @token}
418 419 420
    end
  end

421 422 423 424 425 426 427 428
  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

429 430
    assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true }
    assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true, format: 'js' }
431 432
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
433
      get :negotiate_cross_origin, xhr: true
434 435 436
    end
  end

437 438 439 440 441 442
  def test_should_not_raise_error_if_token_is_not_a_string
    assert_blocked do
      patch :index, params: { custom_authenticity_token: { foo: 'bar' } }
    end
  end

443 444 445 446
  def assert_blocked
    session[:something_like_user_id] = 1
    yield
    assert_nil session[:something_like_user_id], "session values are still present"
447 448
    assert_response :success
  end
449

450 451
  def assert_not_blocked
    assert_nothing_raised { yield }
452 453
    assert_response :success
  end
454 455 456 457 458 459 460 461 462 463

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

  def assert_cross_origin_not_blocked
    assert_not_blocked { yield }
  end
464 465 466 467 468 469 470 471 472 473

  def forgery_protection_origin_check
    old_setting = ActionController::Base.forgery_protection_origin_check
    ActionController::Base.forgery_protection_origin_check = true
    begin
      yield
    ensure
      ActionController::Base.forgery_protection_origin_check = old_setting
    end
  end
474 475
end

476
# OK let's get our test on
477

478
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
479
  include RequestForgeryProtectionTests
J
Jeremy Kemper 已提交
480

481
  setup do
Z
Zuhao Wan 已提交
482
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
483 484 485 486
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
  end

  teardown do
Z
Zuhao Wan 已提交
487
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
488 489
  end

490
  test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
491 492 493 494
    @controller.stub :form_authenticity_token, @token + '<=?' do
      get :meta
      assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
      assert_select 'meta[name=?]', 'csrf-token'
495 496
      regexp = "#{@token}&lt;=\?"
      assert_match(/#{regexp}/, @response.body)
497
    end
J
Jeremy Kemper 已提交
498
  end
499 500
end

501 502 503 504 505
class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
  class NullSessionDummyKeyGenerator
    def generate_key(secret)
      '03312270731a2ed0d11ed091c2338a06'
    end
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
  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
521 522 523 524 525

  test 'should allow reset_session' do
    post :try_to_reset_session
    assert_response :ok
  end
526 527
end

528
class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
529 530 531 532 533 534 535 536
  include RequestForgeryProtectionTests
  def assert_blocked
    assert_raises(ActionController::InvalidAuthenticityToken) do
      yield
    end
  end
end

537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
class PrependProtectForgeryBaseControllerTest < ActionController::TestCase
  PrependTrueController = Class.new(PrependProtectForgeryBaseController) do
    protect_from_forgery prepend: true
  end

  PrependFalseController = Class.new(PrependProtectForgeryBaseController) do
    protect_from_forgery prepend: false
  end

  PrependDefaultController = Class.new(PrependProtectForgeryBaseController) do
    protect_from_forgery
  end

  def test_verify_authenticity_token_is_prepended
    @controller = PrependTrueController.new
    get :index
    expected_callback_order = ["verify_authenticity_token", "custom_action"]
    assert_equal(expected_callback_order, @controller.called_callbacks)
  end

  def test_verify_authenticity_token_is_not_prepended
    @controller = PrependFalseController.new
    get :index
    expected_callback_order = ["custom_action", "verify_authenticity_token"]
    assert_equal(expected_callback_order, @controller.called_callbacks)
  end

564
  def test_verify_authenticity_token_is_not_prepended_by_default
565 566
    @controller = PrependDefaultController.new
    get :index
567
    expected_callback_order = ["custom_action", "verify_authenticity_token"]
568 569 570 571
    assert_equal(expected_callback_order, @controller.called_callbacks)
  end
end

572
class FreeCookieControllerTest < ActionController::TestCase
573 574
  def setup
    @controller = FreeCookieController.new
J
Jeremy Kemper 已提交
575
    @token      = "cf50faa3fe97702ca1ae"
576
    super
577
  end
578

579
  def test_should_not_render_form_with_token_tag
580 581 582 583
    SecureRandom.stub :base64, @token do
      get :index
      assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
    end
584
  end
585

586
  def test_should_not_render_button_to_with_token_tag
587 588 589 590
    SecureRandom.stub :base64, @token do
      get :show_button
      assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
    end
591
  end
592

593
  def test_should_allow_all_methods_without_token
594 595 596 597
    SecureRandom.stub :base64, @token do
      [:post, :patch, :put, :delete].each do |method|
        assert_nothing_raised { send(method, :index)}
      end
598 599
    end
  end
J
Jeremy Kemper 已提交
600 601

  test 'should not emit a csrf-token meta tag' do
602 603 604 605
    SecureRandom.stub :base64, @token do
      get :meta
      assert @response.body.blank?
    end
J
Jeremy Kemper 已提交
606
  end
607
end
608 609 610

class CustomAuthenticityParamControllerTest < ActionController::TestCase
  def setup
611
    super
612 613
    @old_logger = ActionController::Base.logger
    @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
614
    @token = Base64.strict_encode64(SecureRandom.random_bytes(32))
Z
Zuhao Wan 已提交
615
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
616
    ActionController::Base.request_forgery_protection_token = @token
617 618 619
  end

  def teardown
Z
Zuhao Wan 已提交
620
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
621
    super
622 623
  end

624 625 626
  def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
    ActionController::Base.logger = @logger
    begin
627 628 629 630
      @controller.stub :valid_authenticity_token?, :true do
        post :index, params: { custom_token_name: 'foobar' }
        assert_equal 0, @logger.logged(:warn).size
      end
631 632 633 634 635 636 637 638 639
    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
640
      post :index, params: { custom_token_name: 'bazqux' }
641 642 643 644
      assert_equal 1, @logger.logged(:warn).size
    ensure
      ActionController::Base.logger = @old_logger
    end
645 646
  end
end
B
Ben Toews 已提交
647 648 649 650 651 652 653 654 655 656 657 658

class PerFormTokensControllerTest < ActionController::TestCase
  def test_per_form_token_is_same_size_as_global_token
    get :index
    expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH
    actual = @controller.send(:per_form_csrf_token, session, '/path', 'post').size
    assert_equal expected, actual
  end

  def test_accepts_token_for_correct_path_and_method
    get :index

659
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
660

661
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
662 663 664 665 666 667 668 669 670 671 672 673

    # This is required because PATH_INFO isn't reset between requests.
    @request.env['PATH_INFO'] = '/per_form_tokens/post_one'
    assert_nothing_raised do
      post :post_one, params: {custom_authenticity_token: form_token}
    end
    assert_response :success
  end

  def test_rejects_token_for_incorrect_path
    get :index

674
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
675

676
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
677 678 679 680 681 682 683 684 685 686 687

    # This is required because PATH_INFO isn't reset between requests.
    @request.env['PATH_INFO'] = '/per_form_tokens/post_two'
    assert_raises(ActionController::InvalidAuthenticityToken) do
      post :post_two, params: {custom_authenticity_token: form_token}
    end
  end

  def test_rejects_token_for_incorrect_method
    get :index

688
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
689

690
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
691 692 693 694 695 696 697 698

    # This is required because PATH_INFO isn't reset between requests.
    @request.env['PATH_INFO'] = '/per_form_tokens/post_one'
    assert_raises(ActionController::InvalidAuthenticityToken) do
      patch :post_one, params: {custom_authenticity_token: form_token}
    end
  end

699 700 701
  def test_rejects_token_for_incorrect_method_button_to
    get :button_to, params: { form_method: 'delete' }

702
    form_token = assert_presence_and_fetch_form_csrf_token
703

704
    assert_matches_session_token_on_server form_token, 'delete'
705 706 707 708 709 710 711 712

    # This is required because PATH_INFO isn't reset between requests.
    @request.env['PATH_INFO'] = '/per_form_tokens/post_one'
    assert_raises(ActionController::InvalidAuthenticityToken) do
      patch :post_one, params: { custom_authenticity_token: form_token }
    end
  end

713 714 715
  %w{delete post patch}.each do |verb|
    test "Accepts proper token for #{verb} method on button_to tag" do
      get :button_to, params: { form_method: verb }
716

717
      form_token = assert_presence_and_fetch_form_csrf_token
718

719
      assert_matches_session_token_on_server form_token, verb
720

721 722 723 724 725
      # This is required because PATH_INFO isn't reset between requests.
      @request.env['PATH_INFO'] = '/per_form_tokens/post_one'
      assert_nothing_raised do
        send verb, :post_one, params: { custom_authenticity_token: form_token }
      end
726 727 728
    end
  end

B
Ben Toews 已提交
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
  def test_accepts_global_csrf_token
    get :index

    token = @controller.send(:form_authenticity_token)

    # This is required because PATH_INFO isn't reset between requests.
    @request.env['PATH_INFO'] = '/per_form_tokens/post_one'
    assert_nothing_raised do
      post :post_one, params: {custom_authenticity_token: token}
    end
    assert_response :success
  end

  def test_ignores_params
    get :index, params: {form_path: '/per_form_tokens/post_one?foo=bar'}

745
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
746

747
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
748 749 750 751 752 753 754 755 756 757 758 759

    # This is required because PATH_INFO isn't reset between requests.
    @request.env['PATH_INFO'] = '/per_form_tokens/post_one?foo=baz'
    assert_nothing_raised do
      post :post_one, params: {custom_authenticity_token: form_token, baz: 'foo'}
    end
    assert_response :success
  end

  def test_ignores_trailing_slash_during_generation
    get :index, params: {form_path: '/per_form_tokens/post_one/'}

760
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
761 762 763 764 765 766 767 768 769 770 771 772

    # This is required because PATH_INFO isn't reset between requests.
    @request.env['PATH_INFO'] = '/per_form_tokens/post_one'
    assert_nothing_raised do
      post :post_one, params: {custom_authenticity_token: form_token}
    end
    assert_response :success
  end

  def test_ignores_trailing_slash_during_validation
    get :index

773
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
774 775 776 777 778 779 780 781 782 783 784 785

    # This is required because PATH_INFO isn't reset between requests.
    @request.env['PATH_INFO'] = '/per_form_tokens/post_one/'
    assert_nothing_raised do
      post :post_one, params: {custom_authenticity_token: form_token}
    end
    assert_response :success
  end

  def test_method_is_case_insensitive
    get :index, params: {form_method: "POST"}

786
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
787 788 789 790 791 792 793
    # This is required because PATH_INFO isn't reset between requests.
    @request.env['PATH_INFO'] = '/per_form_tokens/post_one/'
    assert_nothing_raised do
      post :post_one, params: {custom_authenticity_token: form_token}
    end
    assert_response :success
  end
794 795 796 797 798 799 800 801 802 803 804 805 806 807 808

  private
    def assert_presence_and_fetch_form_csrf_token
      assert_select 'input[name="custom_authenticity_token"]' do |input|
        form_csrf_token = input.first['value']
        assert_not_nil form_csrf_token
        return form_csrf_token
      end
    end

    def assert_matches_session_token_on_server(form_token, method = 'post')
      actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
      expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', method)
      assert_equal expected, actual
    end
B
Ben Toews 已提交
809
end