request_forgery_protection_test.rb 30.7 KB
Newer Older
1 2
# frozen_string_literal: true

3
require "abstract_unit"
4
require "active_support/log_subscriber/test_helper"
5

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

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

16
  def unsafe
17
    render plain: "pwn"
18
  end
19

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

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

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

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

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

40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
  def form_with_remote
    render inline: "<%= form_with(scope: :some_resource) {} %>"
  end

  def form_with_remote_with_token
    render inline: "<%= form_with(scope: :some_resource, authenticity_token: true) {} %>"
  end

  def form_with_local_with_token
    render inline: "<%= form_with(scope: :some_resource, local: true, authenticity_token: true) {} %>"
  end

  def form_with_remote_with_external_token
    render inline: "<%= form_with(scope: :some_resource, authenticity_token: 'external_token') {} %>"
  end

56
  def same_origin_js
57
    render js: "foo();"
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
  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 74 75
end

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

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

86
class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
87
  protect_from_forgery with: :null_session
88 89

  def signed
90
    cookies.signed[:foo] = "bar"
91
    head :ok
92 93 94
  end

  def encrypted
95
    cookies.encrypted[:foo] = "bar"
96
    head :ok
97
  end
98 99 100

  def try_to_reset_session
    reset_session
101
    head :ok
102
  end
103
end
104

105 106 107 108 109
class PrependProtectForgeryBaseController < ActionController::Base
  before_action :custom_action
  attr_accessor :called_callbacks

  def index
110
    render inline: "OK"
111 112
  end

113
  private
114

115 116 117 118
    def add_called_callback(name)
      @called_callbacks ||= []
      @called_callbacks << name
    end
119

120 121 122
    def custom_action
      add_called_callback("custom_action")
    end
123

124 125 126
    def verify_authenticity_token
      add_called_callback("verify_authenticity_token")
    end
127 128
end

129
class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
130
  self.allow_forgery_protection = false
131

132
  def index
133
    render inline: "<%= form_tag('/') {} %>"
134
  end
135

136
  def show_button
137
    render inline: "<%= button_to('New', '/') %>"
138 139 140
  end
end

141
class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession
142
  def form_authenticity_param
143
    "foobar"
144 145 146
  end
end

B
Ben Toews 已提交
147
class PerFormTokensController < ActionController::Base
148
  protect_from_forgery with: :exception
B
Ben Toews 已提交
149 150 151
  self.per_form_csrf_tokens = true

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

155
  def button_to
156
    render inline: "<%= button_to 'Button', (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>"
157 158
  end

B
Ben Toews 已提交
159
  def post_one
160
    render plain: ""
B
Ben Toews 已提交
161 162 163
  end

  def post_two
164
    render plain: ""
B
Ben Toews 已提交
165 166 167
  end
end

168 169 170 171 172 173 174
class SkipProtectionController < ActionController::Base
  include RequestForgeryProtectionActions
  protect_from_forgery with: :exception
  skip_forgery_protection if: :skip_requested
  attr_accessor :skip_requested
end

175
# common test methods
176
module RequestForgeryProtectionTests
177
  def setup
178
    @token = Base64.strict_encode64("railstestrailstestrailstestrails")
Z
Zuhao Wan 已提交
179
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
180
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
E
Emilio Tagua 已提交
181 182
  end

183
  def teardown
Z
Zuhao Wan 已提交
184
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
185
  end
186

187
  def test_should_render_form_with_token_tag
188 189 190 191
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :index
      end
192
      assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
193
    end
194 195
  end

E
Emilio Tagua 已提交
196
  def test_should_render_button_to_with_token_tag
197 198 199 200
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :show_button
      end
201
      assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
202
    end
E
Emilio Tagua 已提交
203 204
  end

205
  def test_should_render_form_without_token_tag_if_remote
206 207 208
    assert_not_blocked do
      get :form_for_remote
    end
209
    assert_no_match(/authenticity_token/, response.body)
210 211
  end

212 213
  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
214
    begin
215
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
216 217 218
      assert_not_blocked do
        get :form_for_remote
      end
219
      assert_match(/authenticity_token/, response.body)
220
    ensure
221
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
222 223 224
    end
  end

225 226 227 228
  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
229
      assert_not_blocked do
230
        get :form_for_remote_with_external_token
231
      end
232
      assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
233
    ensure
234
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
235
    end
236 237
  end

238 239 240 241
  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
242
    assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
243 244
  end

245
  def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested
246 247 248 249
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :form_for_remote_with_token
      end
250
      assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
251 252 253
    end
  end

254
  def test_should_render_form_with_token_tag_with_authenticity_token_requested
255 256 257 258
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :form_for_with_token
      end
259
      assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
260 261 262
    end
  end

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
  def test_should_render_form_with_with_token_tag_if_remote
    assert_not_blocked do
      get :form_with_remote
    end
    assert_match(/authenticity_token/, response.body)
  end

  def test_should_render_form_with_without_token_tag_if_remote_and_embedding_token_is_off
    original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
    begin
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = false
      assert_not_blocked do
        get :form_with_remote
      end
      assert_no_match(/authenticity_token/, response.body)
    ensure
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
    end
  end

  def test_should_render_form_with_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
      assert_not_blocked do
        get :form_with_remote_with_external_token
      end
      assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
    ensure
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
    end
  end

  def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested
    assert_not_blocked do
      get :form_with_remote_with_external_token
    end
    assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
  end

  def test_should_render_form_with_with_token_tag_if_remote_and_authenticity_token_requested
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :form_with_remote_with_token
      end
      assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
    end
  end

  def test_should_render_form_with_with_token_tag_with_authenticity_token_requested
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked do
        get :form_with_local_with_token
      end
      assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
    end
  end

  def test_should_render_form_with_with_token_tag_if_remote_and_embedding_token_is_on
    original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
    begin
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true

      @controller.stub :form_authenticity_token, @token do
        assert_not_blocked do
          get :form_with_remote
        end
      end
      assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
    ensure
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
    end
  end

E
Emilio Tagua 已提交
337
  def test_should_allow_get
338
    assert_not_blocked { get :index }
E
Emilio Tagua 已提交
339 340
  end

341 342 343 344
  def test_should_allow_head
    assert_not_blocked { head :index }
  end

E
Emilio Tagua 已提交
345
  def test_should_allow_post_without_token_on_unsafe_action
346
    assert_not_blocked { post :unsafe }
347 348
  end

349 350
  def test_should_not_allow_post_without_token
    assert_blocked { post :index }
351 352
  end

353
  def test_should_not_allow_post_without_token_irrespective_of_format
354
    assert_blocked { post :index, format: "xml" }
355 356
  end

357 358 359 360
  def test_should_not_allow_patch_without_token
    assert_blocked { patch :index }
  end

361 362
  def test_should_not_allow_put_without_token
    assert_blocked { put :index }
363
  end
364

365 366
  def test_should_not_allow_delete_without_token
    assert_blocked { delete :index }
367
  end
368

369
  def test_should_not_allow_xhr_post_without_token
370
    assert_blocked { post :index, xhr: true }
371
  end
372

373
  def test_should_allow_post_with_token
374 375 376 377
    session[:_csrf_token] = @token
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
    end
378
  end
379

380
  def test_should_allow_patch_with_token
381 382 383 384
    session[:_csrf_token] = @token
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } }
    end
385 386
  end

387
  def test_should_allow_put_with_token
388 389 390 391
    session[:_csrf_token] = @token
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked { put :index, params: { custom_authenticity_token: @token } }
    end
392
  end
393

394
  def test_should_allow_delete_with_token
395 396 397 398
    session[:_csrf_token] = @token
    @controller.stub :form_authenticity_token, @token do
      assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } }
    end
399
  end
400

401
  def test_should_allow_post_with_token_in_header
402
    session[:_csrf_token] = @token
403
    @request.env["HTTP_X_CSRF_TOKEN"] = @token
404
    assert_not_blocked { post :index }
405
  end
406

407
  def test_should_allow_delete_with_token_in_header
408
    session[:_csrf_token] = @token
409
    @request.env["HTTP_X_CSRF_TOKEN"] = @token
410
    assert_not_blocked { delete :index }
411
  end
412

413
  def test_should_allow_patch_with_token_in_header
414
    session[:_csrf_token] = @token
415
    @request.env["HTTP_X_CSRF_TOKEN"] = @token
416 417 418
    assert_not_blocked { patch :index }
  end

419
  def test_should_allow_put_with_token_in_header
420
    session[:_csrf_token] = @token
421
    @request.env["HTTP_X_CSRF_TOKEN"] = @token
422
    assert_not_blocked { put :index }
423
  end
424

425 426 427 428 429
  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
430
          @request.set_header "HTTP_ORIGIN", "http://test.host"
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
          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
449 450 451 452
    old_logger = ActionController::Base.logger
    logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
    ActionController::Base.logger = logger

453 454 455 456
    forgery_protection_origin_check do
      session[:_csrf_token] = @token
      @controller.stub :form_authenticity_token, @token do
        assert_blocked do
457
          @request.set_header "HTTP_ORIGIN", "http://bad.host"
458 459 460 461
          post :index, params: { custom_authenticity_token: @token }
        end
      end
    end
462 463 464 465 466 467 468

    assert_match(
      "HTTP Origin header (http://bad.host) didn't match request.base_url (http://test.host)",
      logger.logged(:warn).last
    )
  ensure
    ActionController::Base.logger = old_logger
469 470
  end

471 472 473 474 475 476 477 478 479 480
  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 已提交
481
    ensure
482 483 484
      ActionController::Base.logger = old_logger
    end
  end
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500

  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
501

502 503
  def test_should_only_allow_same_origin_js_get_with_xhr_header
    assert_cross_origin_blocked { get :same_origin_js }
504
    assert_cross_origin_blocked { get :same_origin_js, format: "js" }
505
    assert_cross_origin_blocked do
506
      @request.accept = "text/javascript"
507 508 509
      get :negotiate_same_origin
    end

510
    assert_cross_origin_not_blocked { get :same_origin_js, xhr: true }
511
    assert_cross_origin_not_blocked { get :same_origin_js, xhr: true, format: "js" }
512
    assert_cross_origin_not_blocked do
513
      @request.accept = "text/javascript"
514
      get :negotiate_same_origin, xhr: true
515 516 517
    end
  end

518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
  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

549 550
  # Allow non-GET requests since GET is all a remote <script> tag can muster.
  def test_should_allow_non_get_js_without_xhr_header
551
    session[:_csrf_token] = @token
552
    assert_cross_origin_not_blocked { post :same_origin_js, params: { custom_authenticity_token: @token } }
553
    assert_cross_origin_not_blocked { post :same_origin_js, params: { format: "js", custom_authenticity_token: @token } }
554
    assert_cross_origin_not_blocked do
555
      @request.accept = "text/javascript"
556
      post :negotiate_same_origin, params: { custom_authenticity_token: @token }
557 558 559
    end
  end

560 561
  def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled
    assert_cross_origin_not_blocked { get :cross_origin_js }
562
    assert_cross_origin_not_blocked { get :cross_origin_js, format: "js" }
563
    assert_cross_origin_not_blocked do
564
      @request.accept = "text/javascript"
565 566 567
      get :negotiate_cross_origin
    end

568
    assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true }
569
    assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true, format: "js" }
570
    assert_cross_origin_not_blocked do
571
      @request.accept = "text/javascript"
572
      get :negotiate_cross_origin, xhr: true
573 574 575
    end
  end

576 577
  def test_should_not_raise_error_if_token_is_not_a_string
    assert_blocked do
578
      patch :index, params: { custom_authenticity_token: { foo: "bar" } }
579 580 581
    end
  end

582 583 584 585
  def assert_blocked
    session[:something_like_user_id] = 1
    yield
    assert_nil session[:something_like_user_id], "session values are still present"
586 587
    assert_response :success
  end
588

589 590
  def assert_not_blocked
    assert_nothing_raised { yield }
591 592
    assert_response :success
  end
593 594 595 596 597 598 599 600 601 602

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

  def assert_cross_origin_not_blocked
    assert_not_blocked { yield }
  end
603 604 605 606 607 608 609 610 611 612

  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
613 614
end

615
# OK let's get our test on
616

617
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
618
  include RequestForgeryProtectionTests
J
Jeremy Kemper 已提交
619

620 621
  test "should emit a csrf-param meta tag and a csrf-token meta tag" do
    @controller.stub :form_authenticity_token, @token + "<=?" do
622
      get :meta
623 624
      assert_select "meta[name=?][content=?]", "csrf-param", "custom_authenticity_token"
      assert_select "meta[name=?]", "csrf-token"
625 626
      regexp = "#{@token}&lt;=\?"
      assert_match(/#{regexp}/, @response.body)
627
    end
J
Jeremy Kemper 已提交
628
  end
629 630
end

631 632 633
class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
  class NullSessionDummyKeyGenerator
    def generate_key(secret)
634
      "03312270731a2ed0d11ed091c2338a06"
635
    end
636 637 638 639 640 641
  end

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

642
  test "should allow to set signed cookies" do
643 644 645 646
    post :signed
    assert_response :ok
  end

647
  test "should allow to set encrypted cookies" do
648 649 650
    post :encrypted
    assert_response :ok
  end
651

652
  test "should allow reset_session" do
653 654 655
    post :try_to_reset_session
    assert_response :ok
  end
656 657
end

658
class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
659 660 661 662 663 664 665 666
  include RequestForgeryProtectionTests
  def assert_blocked
    assert_raises(ActionController::InvalidAuthenticityToken) do
      yield
    end
  end
end

667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
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

694
  def test_verify_authenticity_token_is_not_prepended_by_default
695 696
    @controller = PrependDefaultController.new
    get :index
697
    expected_callback_order = ["custom_action", "verify_authenticity_token"]
698 699 700 701
    assert_equal(expected_callback_order, @controller.called_callbacks)
  end
end

702
class FreeCookieControllerTest < ActionController::TestCase
703 704
  def setup
    @controller = FreeCookieController.new
J
Jeremy Kemper 已提交
705
    @token      = "cf50faa3fe97702ca1ae"
706
    super
707
  end
708

709
  def test_should_not_render_form_with_token_tag
710 711
    SecureRandom.stub :base64, @token do
      get :index
712
      assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
713
    end
714
  end
715

716
  def test_should_not_render_button_to_with_token_tag
717 718
    SecureRandom.stub :base64, @token do
      get :show_button
719
      assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
720
    end
721
  end
722

723
  def test_should_allow_all_methods_without_token
724 725
    SecureRandom.stub :base64, @token do
      [:post, :patch, :put, :delete].each do |method|
726
        assert_nothing_raised { send(method, :index) }
727
      end
728 729
    end
  end
J
Jeremy Kemper 已提交
730

731
  test "should not emit a csrf-token meta tag" do
732 733 734 735
    SecureRandom.stub :base64, @token do
      get :meta
      assert @response.body.blank?
    end
J
Jeremy Kemper 已提交
736
  end
737
end
738 739 740

class CustomAuthenticityParamControllerTest < ActionController::TestCase
  def setup
741
    super
742 743
    @old_logger = ActionController::Base.logger
    @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
744
    @token = Base64.strict_encode64(SecureRandom.random_bytes(32))
Z
Zuhao Wan 已提交
745
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
746
    ActionController::Base.request_forgery_protection_token = @token
747 748 749
  end

  def teardown
Z
Zuhao Wan 已提交
750
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
751
    super
752 753
  end

754 755 756
  def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
    ActionController::Base.logger = @logger
    begin
757
      @controller.stub :valid_authenticity_token?, :true do
758
        post :index, params: { custom_token_name: "foobar" }
759 760
        assert_equal 0, @logger.logged(:warn).size
      end
761 762 763 764 765 766 767 768 769
    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
770
      post :index, params: { custom_token_name: "bazqux" }
771 772 773 774
      assert_equal 1, @logger.logged(:warn).size
    ensure
      ActionController::Base.logger = @old_logger
    end
775 776
  end
end
B
Ben Toews 已提交
777 778

class PerFormTokensControllerTest < ActionController::TestCase
779 780 781 782 783 784 785 786 787
  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 已提交
788 789 790
  def test_per_form_token_is_same_size_as_global_token
    get :index
    expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH
791
    actual = @controller.send(:per_form_csrf_token, session, "/path", "post").size
B
Ben Toews 已提交
792 793 794 795 796 797
    assert_equal expected, actual
  end

  def test_accepts_token_for_correct_path_and_method
    get :index

798
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
799

800
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
801 802

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

  def test_rejects_token_for_incorrect_path
    get :index

813
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
814

815
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
816 817

    # This is required because PATH_INFO isn't reset between requests.
818
    @request.env["PATH_INFO"] = "/per_form_tokens/post_two"
B
Ben Toews 已提交
819
    assert_raises(ActionController::InvalidAuthenticityToken) do
820
      post :post_two, params: { custom_authenticity_token: form_token }
B
Ben Toews 已提交
821 822 823 824 825 826
    end
  end

  def test_rejects_token_for_incorrect_method
    get :index

827
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
828

829
    assert_matches_session_token_on_server form_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
    assert_raises(ActionController::InvalidAuthenticityToken) do
834
      patch :post_one, params: { custom_authenticity_token: form_token }
B
Ben Toews 已提交
835 836 837
    end
  end

838
  def test_rejects_token_for_incorrect_method_button_to
839
    get :button_to, params: { form_method: "delete" }
840

841
    form_token = assert_presence_and_fetch_form_csrf_token
842

843
    assert_matches_session_token_on_server form_token, "delete"
844 845

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

852 853 854 855 856
  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

857
    assert_matches_session_token_on_server form_token, "post"
858 859

    # This is required because PATH_INFO isn't reset between requests.
860
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
861 862 863 864 865
    assert_nothing_raised do
      post :post_one, params: { custom_authenticity_token: form_token }
    end
  end

866 867 868
  %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 }
869

870
      form_token = assert_presence_and_fetch_form_csrf_token
871

872
      assert_matches_session_token_on_server form_token, verb
873

874
      # This is required because PATH_INFO isn't reset between requests.
875
      @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
876 877 878
      assert_nothing_raised do
        send verb, :post_one, params: { custom_authenticity_token: form_token }
      end
879 880 881
    end
  end

B
Ben Toews 已提交
882 883 884 885 886 887
  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.
888
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
B
Ben Toews 已提交
889
    assert_nothing_raised do
890
      post :post_one, params: { custom_authenticity_token: token }
B
Ben Toews 已提交
891 892 893 894 895
    end
    assert_response :success
  end

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

898
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
899

900
    assert_matches_session_token_on_server form_token
B
Ben Toews 已提交
901 902

    # This is required because PATH_INFO isn't reset between requests.
903
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one?foo=baz"
B
Ben Toews 已提交
904
    assert_nothing_raised do
905
      post :post_one, params: { custom_authenticity_token: form_token, baz: "foo" }
B
Ben Toews 已提交
906 907 908 909 910
    end
    assert_response :success
  end

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

913
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
914 915

    # This is required because PATH_INFO isn't reset between requests.
916
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
B
Ben Toews 已提交
917
    assert_nothing_raised do
918
      post :post_one, params: { custom_authenticity_token: form_token }
B
Ben Toews 已提交
919 920 921 922
    end
    assert_response :success
  end

923
  def test_ignores_origin_during_generation
924
    get :index, params: { form_path: "https://example.com/per_form_tokens/post_one/" }
925 926 927 928

    form_token = assert_presence_and_fetch_form_csrf_token

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

B
Ben Toews 已提交
936 937 938
  def test_ignores_trailing_slash_during_validation
    get :index

939
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
940 941

    # This is required because PATH_INFO isn't reset between requests.
942
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
B
Ben Toews 已提交
943
    assert_nothing_raised do
944
      post :post_one, params: { custom_authenticity_token: form_token }
B
Ben Toews 已提交
945 946 947 948 949
    end
    assert_response :success
  end

  def test_method_is_case_insensitive
950
    get :index, params: { form_method: "POST" }
B
Ben Toews 已提交
951

952
    form_token = assert_presence_and_fetch_form_csrf_token
B
Ben Toews 已提交
953
    # This is required because PATH_INFO isn't reset between requests.
954
    @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
B
Ben Toews 已提交
955
    assert_nothing_raised do
956
      post :post_one, params: { custom_authenticity_token: form_token }
B
Ben Toews 已提交
957 958 959
    end
    assert_response :success
  end
960 961 962 963

  private
    def assert_presence_and_fetch_form_csrf_token
      assert_select 'input[name="custom_authenticity_token"]' do |input|
964
        form_csrf_token = input.first["value"]
965 966 967 968 969
        assert_not_nil form_csrf_token
        return form_csrf_token
      end
    end

970
    def assert_matches_session_token_on_server(form_token, method = "post")
971
      actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
972
      expected = @controller.send(:per_form_csrf_token, session, "/per_form_tokens/post_one", method)
973 974
      assert_equal expected, actual
    end
B
Ben Toews 已提交
975
end
976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998

class SkipProtectionControllerTest < ActionController::TestCase
  def test_should_not_allow_post_without_token_when_not_skipping
    @controller.skip_requested = false
    assert_blocked { post :index }
  end

  def test_should_allow_post_without_token_when_skipping
    @controller.skip_requested = true
    assert_not_blocked { post :index }
  end

  def assert_blocked
    assert_raises(ActionController::InvalidAuthenticityToken) do
      yield
    end
  end

  def assert_not_blocked
    assert_nothing_raised { yield }
    assert_response :success
  end
end