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
# common controller actions
module RequestForgeryProtectionActions
  def index
7
    render inline: "<%= form_tag('/') {} %>"
8
  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
  def form_for_remote
23
    render inline: "<%= form_for(:some_resource, :remote => true ) {} %>"
24 25
  end

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

30
  def form_for_with_token
31
    render inline: "<%= form_for(:some_resource, :authenticity_token => true ) {} %>"
32 33 34
  end

  def form_for_remote_with_external_token
35
    render inline: "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>"
36 37
  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
  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
55 56 57
end

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

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

68
class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
69
  protect_from_forgery with: :null_session
70 71

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

  def encrypted
77
    cookies.encrypted[:foo] = "bar"
78
    head :ok
79
  end
80 81 82

  def try_to_reset_session
    reset_session
83
    head :ok
84
  end
85
end
86

87 88 89 90 91
class PrependProtectForgeryBaseController < ActionController::Base
  before_action :custom_action
  attr_accessor :called_callbacks

  def index
92
    render inline: "OK"
93 94 95 96
  end

  protected

97 98 99 100
    def add_called_callback(name)
      @called_callbacks ||= []
      @called_callbacks << name
    end
101

102 103 104
    def custom_action
      add_called_callback("custom_action")
    end
105

106 107 108
    def verify_authenticity_token
      add_called_callback("verify_authenticity_token")
    end
109 110
end

111
class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
112
  self.allow_forgery_protection = false
113

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

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

123
class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession
124
  def form_authenticity_param
125
    "foobar"
126 127 128
  end
end

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

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

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

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

  def post_two
146
    render plain: ""
B
Ben Toews 已提交
147 148 149
  end
end

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

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

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

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

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

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

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

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

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

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

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

242 243 244 245
  def test_should_allow_head
    assert_not_blocked { head :index }
  end

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

250 251
  def test_should_not_allow_post_without_token
    assert_blocked { post :index }
252 253
  end

254
  def test_should_not_allow_post_without_token_irrespective_of_format
255
    assert_blocked { post :index, format: "xml" }
256 257
  end

258 259 260 261
  def test_should_not_allow_patch_without_token
    assert_blocked { patch :index }
  end

262 263
  def test_should_not_allow_put_without_token
    assert_blocked { put :index }
264
  end
265

266 267
  def test_should_not_allow_delete_without_token
    assert_blocked { delete :index }
268
  end
269

270
  def test_should_not_allow_xhr_post_without_token
271
    assert_blocked { post :index, xhr: true }
272
  end
273

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

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

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

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

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

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

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

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

326 327 328 329 330
  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
331
          @request.set_header "HTTP_ORIGIN", "http://test.host"
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
          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
354
          @request.set_header "HTTP_ORIGIN", "http://bad.host"
355 356 357 358 359 360
          post :index, params: { custom_authenticity_token: @token }
        end
      end
    end
  end

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

  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
391

392 393
  def test_should_only_allow_same_origin_js_get_with_xhr_header
    assert_cross_origin_blocked { get :same_origin_js }
394
    assert_cross_origin_blocked { get :same_origin_js, format: "js" }
395
    assert_cross_origin_blocked do
396
      @request.accept = "text/javascript"
397 398 399
      get :negotiate_same_origin
    end

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

408 409 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
  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

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

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

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

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

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

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

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

  def assert_cross_origin_not_blocked
    assert_not_blocked { yield }
  end
493 494 495 496 497 498 499 500 501 502

  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
503 504
end

505
# OK let's get our test on
506

507
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
508
  include RequestForgeryProtectionTests
J
Jeremy Kemper 已提交
509

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

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

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

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

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

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

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

557 558 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
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

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

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

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

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

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

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

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

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

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

class PerFormTokensControllerTest < ActionController::TestCase
669 670 671 672 673 674 675 676 677
  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 已提交
678 679 680
  def test_per_form_token_is_same_size_as_global_token
    get :index
    expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH
681
    actual = @controller.send(:per_form_csrf_token, session, "/path", "post").size
B
Ben Toews 已提交
682 683 684 685 686 687
    assert_equal expected, actual
  end

  def test_accepts_token_for_correct_path_and_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

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

703
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
704

705
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
706 707

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

717
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
718

719
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
720 721

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

728
  def test_rejects_token_for_incorrect_method_button_to
729
    get :button_to, params: { form_method: "delete" }
730

731
    form_token = assert_presence_and_fetch_form_csrf_token
732

733
    assert_matches_session_token_on_server form_token, "delete"
734 735

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

742 743 744 745 746
  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

747
    assert_matches_session_token_on_server form_token, "post"
748 749

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

756 757 758
  %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 }
759

760
      form_token = assert_presence_and_fetch_form_csrf_token
761

762
      assert_matches_session_token_on_server form_token, verb
763

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

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

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

788
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
789

790
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
791 792

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

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

803
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
804 805

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

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

    form_token = assert_presence_and_fetch_form_csrf_token

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

B
Ben Toews 已提交
826 827 828
  def test_ignores_trailing_slash_during_validation
    get :index

829
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
830 831

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

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

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

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