request_forgery_protection_test.rb 26.2 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
  def same_origin_js
39
    render js: "foo();"
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
  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
class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
  protect_from_forgery :with => :null_session

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

  def encrypted
78
    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
class PrependProtectForgeryBaseController < ActionController::Base
  before_action :custom_action
  attr_accessor :called_callbacks

  def index
93
    render inline: "OK"
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
  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
  def form_authenticity_param
127
    "foobar"
128 129 130
  end
end

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

  def index
136
    render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>"
B
Ben Toews 已提交
137 138
  end

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

B
Ben Toews 已提交
143
  def post_one
144
    render plain: ""
B
Ben Toews 已提交
145 146 147
  end

  def post_two
148
    render plain: ""
B
Ben Toews 已提交
149 150 151
  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
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :index
      end
169
      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
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :show_button
      end
178
      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
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :form_for_remote_with_token
      end
227
      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
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :form_for_with_token
      end
236
      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
    @request.env["HTTP_X_CSRF_TOKEN"] = @token
307
    assert_not_blocked { post :index }
308
  end
309

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

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

322
  def test_should_allow_put_with_token_in_header
323
    session[:_csrf_token] = @token
324
    @request.env["HTTP_X_CSRF_TOKEN"] = @token
325
    assert_not_blocked { put :index }
326
  end
327

328 329 330 331 332
  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
333
          @request.set_header "HTTP_ORIGIN", "http://test.host"
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
          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
356
          @request.set_header "HTTP_ORIGIN", "http://bad.host"
357 358 359 360 361 362
          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
  def test_should_only_allow_same_origin_js_get_with_xhr_header
    assert_cross_origin_blocked { get :same_origin_js }
396
    assert_cross_origin_blocked { get :same_origin_js, format: "js" }
397
    assert_cross_origin_blocked do
398
      @request.accept = "text/javascript"
399 400 401
      get :negotiate_same_origin
    end

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

410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
  def test_should_warn_on_not_same_origin_js
    old_logger = ActionController::Base.logger
    logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
    ActionController::Base.logger = logger

    begin
      assert_cross_origin_blocked { get :same_origin_js }

      assert_equal 1, logger.logged(:warn).size
      assert_match(/<script> tag on another site requested protected JavaScript/, logger.logged(:warn).last)
    ensure
      ActionController::Base.logger = old_logger
    end
  end

  def test_should_not_warn_if_csrf_logging_disabled_and_not_same_origin_js
    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_cross_origin_blocked { get :same_origin_js }

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

441 442
  # Allow non-GET requests since GET is all a remote <script> tag can muster.
  def test_should_allow_non_get_js_without_xhr_header
443
    session[:_csrf_token] = @token
444
    assert_cross_origin_not_blocked { post :same_origin_js, params: { custom_authenticity_token: @token } }
445
    assert_cross_origin_not_blocked { post :same_origin_js, params: { format: "js", custom_authenticity_token: @token } }
446
    assert_cross_origin_not_blocked do
447
      @request.accept = "text/javascript"
448
      post :negotiate_same_origin, params: { custom_authenticity_token: @token}
449 450 451
    end
  end

452 453
  def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled
    assert_cross_origin_not_blocked { get :cross_origin_js }
454
    assert_cross_origin_not_blocked { get :cross_origin_js, format: "js" }
455
    assert_cross_origin_not_blocked do
456
      @request.accept = "text/javascript"
457 458 459
      get :negotiate_cross_origin
    end

460
    assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true }
461
    assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true, format: "js" }
462
    assert_cross_origin_not_blocked do
463
      @request.accept = "text/javascript"
464
      get :negotiate_cross_origin, xhr: true
465 466 467
    end
  end

468 469
  def test_should_not_raise_error_if_token_is_not_a_string
    assert_blocked do
470
      patch :index, params: { custom_authenticity_token: { foo: "bar" } }
471 472 473
    end
  end

474 475 476 477
  def assert_blocked
    session[:something_like_user_id] = 1
    yield
    assert_nil session[:something_like_user_id], "session values are still present"
478 479
    assert_response :success
  end
480

481 482
  def assert_not_blocked
    assert_nothing_raised { yield }
483 484
    assert_response :success
  end
485 486 487 488 489 490 491 492 493 494

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

  def assert_cross_origin_not_blocked
    assert_not_blocked { yield }
  end
495 496 497 498 499 500 501 502 503 504

  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
505 506
end

507
# OK let's get our test on
508

509
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
510
  include RequestForgeryProtectionTests
J
Jeremy Kemper 已提交
511

512 513
  test "should emit a csrf-param meta tag and a csrf-token meta tag" do
    @controller.stub :form_authenticity_token, @token + "<=?" do
514
      get :meta
515 516
      assert_select "meta[name=?][content=?]", "csrf-param", "custom_authenticity_token"
      assert_select "meta[name=?]", "csrf-token"
517 518
      regexp = "#{@token}&lt;=\?"
      assert_match(/#{regexp}/, @response.body)
519
    end
J
Jeremy Kemper 已提交
520
  end
521 522
end

523 524 525
class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
  class NullSessionDummyKeyGenerator
    def generate_key(secret)
526
      "03312270731a2ed0d11ed091c2338a06"
527
    end
528 529 530 531 532 533
  end

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

534
  test "should allow to set signed cookies" do
535 536 537 538
    post :signed
    assert_response :ok
  end

539
  test "should allow to set encrypted cookies" do
540 541 542
    post :encrypted
    assert_response :ok
  end
543

544
  test "should allow reset_session" do
545 546 547
    post :try_to_reset_session
    assert_response :ok
  end
548 549
end

550
class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
551 552 553 554 555 556 557 558
  include RequestForgeryProtectionTests
  def assert_blocked
    assert_raises(ActionController::InvalidAuthenticityToken) do
      yield
    end
  end
end

559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
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

586
  def test_verify_authenticity_token_is_not_prepended_by_default
587 588
    @controller = PrependDefaultController.new
    get :index
589
    expected_callback_order = ["custom_action", "verify_authenticity_token"]
590 591 592 593
    assert_equal(expected_callback_order, @controller.called_callbacks)
  end
end

594
class FreeCookieControllerTest < ActionController::TestCase
595 596
  def setup
    @controller = FreeCookieController.new
J
Jeremy Kemper 已提交
597
    @token      = "cf50faa3fe97702ca1ae"
598
    super
599
  end
600

601
  def test_should_not_render_form_with_token_tag
602 603
    SecureRandom.stub :base64, @token do
      get :index
604
      assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
605
    end
606
  end
607

608
  def test_should_not_render_button_to_with_token_tag
609 610
    SecureRandom.stub :base64, @token do
      get :show_button
611
      assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
612
    end
613
  end
614

615
  def test_should_allow_all_methods_without_token
616 617 618 619
    SecureRandom.stub :base64, @token do
      [:post, :patch, :put, :delete].each do |method|
        assert_nothing_raised { send(method, :index)}
      end
620 621
    end
  end
J
Jeremy Kemper 已提交
622

623
  test "should not emit a csrf-token meta tag" do
624 625 626 627
    SecureRandom.stub :base64, @token do
      get :meta
      assert @response.body.blank?
    end
J
Jeremy Kemper 已提交
628
  end
629
end
630 631 632

class CustomAuthenticityParamControllerTest < ActionController::TestCase
  def setup
633
    super
634 635
    @old_logger = ActionController::Base.logger
    @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
636
    @token = Base64.strict_encode64(SecureRandom.random_bytes(32))
Z
Zuhao Wan 已提交
637
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
638
    ActionController::Base.request_forgery_protection_token = @token
639 640 641
  end

  def teardown
Z
Zuhao Wan 已提交
642
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
643
    super
644 645
  end

646 647 648
  def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
    ActionController::Base.logger = @logger
    begin
649
      @controller.stub :valid_authenticity_token?, :true do
650
        post :index, params: { custom_token_name: "foobar" }
651 652
        assert_equal 0, @logger.logged(:warn).size
      end
653 654 655 656 657 658 659 660 661
    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
662
      post :index, params: { custom_token_name: "bazqux" }
663 664 665 666
      assert_equal 1, @logger.logged(:warn).size
    ensure
      ActionController::Base.logger = @old_logger
    end
667 668
  end
end
B
Ben Toews 已提交
669 670

class PerFormTokensControllerTest < ActionController::TestCase
671 672 673 674 675 676 677 678 679
  def setup
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
  end

  def teardown
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
  end

B
Ben Toews 已提交
680 681 682
  def test_per_form_token_is_same_size_as_global_token
    get :index
    expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH
683
    actual = @controller.send(:per_form_csrf_token, session, "/path", "post").size
B
Ben Toews 已提交
684 685 686 687 688 689
    assert_equal expected, actual
  end

  def test_accepts_token_for_correct_path_and_method
    get :index

690
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
691

692
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
693 694

    # This is required because PATH_INFO isn't reset between requests.
695
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
B
Ben Toews 已提交
696 697 698 699 700 701 702 703 704
    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

705
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
706

707
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
708 709

    # This is required because PATH_INFO isn't reset between requests.
710
    @request.env["PATH_INFO"] = "/per_form_tokens/post_two"
B
Ben Toews 已提交
711 712 713 714 715 716 717 718
    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

719
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
720

721
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
722 723

    # This is required because PATH_INFO isn't reset between requests.
724
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
B
Ben Toews 已提交
725 726 727 728 729
    assert_raises(ActionController::InvalidAuthenticityToken) do
      patch :post_one, params: {custom_authenticity_token: form_token}
    end
  end

730
  def test_rejects_token_for_incorrect_method_button_to
731
    get :button_to, params: { form_method: "delete" }
732

733
    form_token = assert_presence_and_fetch_form_csrf_token
734

735
    assert_matches_session_token_on_server form_token, "delete"
736 737

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

744 745 746 747 748
  test "Accepts proper token for implicit post method on button_to tag" do
    get :button_to

    form_token = assert_presence_and_fetch_form_csrf_token

749
    assert_matches_session_token_on_server form_token, "post"
750 751

    # This is required because PATH_INFO isn't reset between requests.
752
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
753 754 755 756 757
    assert_nothing_raised do
      post :post_one, params: { custom_authenticity_token: form_token }
    end
  end

758 759 760
  %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 }
761

762
      form_token = assert_presence_and_fetch_form_csrf_token
763

764
      assert_matches_session_token_on_server form_token, verb
765

766
      # This is required because PATH_INFO isn't reset between requests.
767
      @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
768 769 770
      assert_nothing_raised do
        send verb, :post_one, params: { custom_authenticity_token: form_token }
      end
771 772 773
    end
  end

B
Ben Toews 已提交
774 775 776 777 778 779
  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.
780
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
B
Ben Toews 已提交
781 782 783 784 785 786 787
    assert_nothing_raised do
      post :post_one, params: {custom_authenticity_token: token}
    end
    assert_response :success
  end

  def test_ignores_params
788
    get :index, params: {form_path: "/per_form_tokens/post_one?foo=bar"}
B
Ben Toews 已提交
789

790
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
791

792
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
793 794

    # This is required because PATH_INFO isn't reset between requests.
795
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one?foo=baz"
B
Ben Toews 已提交
796
    assert_nothing_raised do
797
      post :post_one, params: {custom_authenticity_token: form_token, baz: "foo"}
B
Ben Toews 已提交
798 799 800 801 802
    end
    assert_response :success
  end

  def test_ignores_trailing_slash_during_generation
803
    get :index, params: {form_path: "/per_form_tokens/post_one/"}
B
Ben Toews 已提交
804

805
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
806 807

    # This is required because PATH_INFO isn't reset between requests.
808
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
B
Ben Toews 已提交
809 810 811 812 813 814
    assert_nothing_raised do
      post :post_one, params: {custom_authenticity_token: form_token}
    end
    assert_response :success
  end

815
  def test_ignores_origin_during_generation
816
    get :index, params: {form_path: "https://example.com/per_form_tokens/post_one/"}
817 818 819 820

    form_token = assert_presence_and_fetch_form_csrf_token

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

B
Ben Toews 已提交
828 829 830
  def test_ignores_trailing_slash_during_validation
    get :index

831
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
832 833

    # This is required because PATH_INFO isn't reset between requests.
834
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
B
Ben Toews 已提交
835 836 837 838 839 840 841 842 843
    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"}

844
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
845
    # This is required because PATH_INFO isn't reset between requests.
846
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
B
Ben Toews 已提交
847 848 849 850 851
    assert_nothing_raised do
      post :post_one, params: {custom_authenticity_token: form_token}
    end
    assert_response :success
  end
852 853 854 855

  private
    def assert_presence_and_fetch_form_csrf_token
      assert_select 'input[name="custom_authenticity_token"]' do |input|
856
        form_csrf_token = input.first["value"]
857 858 859 860 861
        assert_not_nil form_csrf_token
        return form_csrf_token
      end
    end

862
    def assert_matches_session_token_on_server(form_token, method = "post")
863
      actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
864
      expected = @controller.send(:per_form_csrf_token, session, "/per_form_tokens/post_one", method)
865 866
      assert_equal expected, actual
    end
B
Ben Toews 已提交
867
end