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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  def cross_origin_js
    same_origin_js
  end

  def negotiate_cross_origin
    negotiate_same_origin
  end

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

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

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

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

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

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

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

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
class PrependProtectForgeryBaseController < ActionController::Base
  before_action :custom_action
  attr_accessor :called_callbacks

  def index
    render inline: 'OK'
  end

  protected

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


  def custom_action
    add_called_callback("custom_action")
  end

  def verify_authenticity_token
    add_called_callback("verify_authenticity_token")
  end
end

131
class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
132
  self.allow_forgery_protection = false
133

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

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

143
class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession
144 145 146 147 148
  def form_authenticity_param
    'foobar'
  end
end

149
# common test methods
150
module RequestForgeryProtectionTests
151 152
  def setup
    @token      = "cf50faa3fe97702ca1ae"
153 154 155
    @controller.stubs(:form_authenticity_token).returns(@token)
    @controller.stubs(:valid_authenticity_token?).with{ |_, t| t == @token }.returns(true)
    @controller.stubs(:valid_authenticity_token?).with{ |_, t| t != @token }.returns(false)
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 165 166 167
  def test_should_render_form_with_token_tag
    assert_not_blocked do
      get :index
    end
168
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
169 170
  end

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

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

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

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

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

218 219 220 221
  def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested
    assert_not_blocked do
      get :form_for_remote_with_token
    end
222
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
223 224
  end

225 226 227 228
  def test_should_render_form_with_token_tag_with_authenticity_token_requested
    assert_not_blocked do
      get :form_for_with_token
    end
229
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
230 231
  end

E
Emilio Tagua 已提交
232
  def test_should_allow_get
233
    assert_not_blocked { get :index }
E
Emilio Tagua 已提交
234 235
  end

236 237 238 239
  def test_should_allow_head
    assert_not_blocked { head :index }
  end

E
Emilio Tagua 已提交
240
  def test_should_allow_post_without_token_on_unsafe_action
241
    assert_not_blocked { post :unsafe }
242 243
  end

244 245
  def test_should_not_allow_post_without_token
    assert_blocked { post :index }
246 247
  end

248
  def test_should_not_allow_post_without_token_irrespective_of_format
249
    assert_blocked { post :index, format: 'xml' }
250 251
  end

252 253 254 255
  def test_should_not_allow_patch_without_token
    assert_blocked { patch :index }
  end

256 257
  def test_should_not_allow_put_without_token
    assert_blocked { put :index }
258
  end
259

260 261
  def test_should_not_allow_delete_without_token
    assert_blocked { delete :index }
262
  end
263

264 265
  def test_should_not_allow_xhr_post_without_token
    assert_blocked { xhr :post, :index }
266
  end
267

268
  def test_should_allow_post_with_token
269
    assert_not_blocked { post :index, :custom_authenticity_token => @token }
270
  end
271

272 273 274 275
  def test_should_allow_patch_with_token
    assert_not_blocked { patch :index, :custom_authenticity_token => @token }
  end

276
  def test_should_allow_put_with_token
277
    assert_not_blocked { put :index, :custom_authenticity_token => @token }
278
  end
279

280
  def test_should_allow_delete_with_token
281
    assert_not_blocked { delete :index, :custom_authenticity_token => @token }
282
  end
283

284 285 286
  def test_should_allow_post_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { post :index }
287
  end
288

289 290 291
  def test_should_allow_delete_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { delete :index }
292
  end
293

294 295 296 297 298
  def test_should_allow_patch_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { patch :index }
  end

299 300 301
  def test_should_allow_put_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { put :index }
302
  end
303

304 305 306 307 308 309 310 311 312 313
  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 已提交
314
    ensure
315 316 317
      ActionController::Base.logger = old_logger
    end
  end
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333

  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
334

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

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

351 352 353 354 355 356 357 358 359 360
  # Allow non-GET requests since GET is all a remote <script> tag can muster.
  def test_should_allow_non_get_js_without_xhr_header
    assert_cross_origin_not_blocked { post :same_origin_js, custom_authenticity_token: @token }
    assert_cross_origin_not_blocked { post :same_origin_js, format: 'js', custom_authenticity_token: @token }
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
      post :negotiate_same_origin, custom_authenticity_token: @token
    end
  end

361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
  def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled
    assert_cross_origin_not_blocked { get :cross_origin_js }
    assert_cross_origin_not_blocked { get :cross_origin_js, format: 'js' }
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
      get :negotiate_cross_origin
    end

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

377 378 379 380
  def assert_blocked
    session[:something_like_user_id] = 1
    yield
    assert_nil session[:something_like_user_id], "session values are still present"
381 382
    assert_response :success
  end
383

384 385
  def assert_not_blocked
    assert_nothing_raised { yield }
386 387
    assert_response :success
  end
388 389 390 391 392 393 394 395 396 397

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

  def assert_cross_origin_not_blocked
    assert_not_blocked { yield }
  end
398 399
end

400
# OK let's get our test on
401

402
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
403
  include RequestForgeryProtectionTests
J
Jeremy Kemper 已提交
404

405
  setup do
Z
Zuhao Wan 已提交
406
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
407 408 409 410
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
  end

  teardown do
Z
Zuhao Wan 已提交
411
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
412 413
  end

414
  test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
415
    @controller.stubs(:form_authenticity_token).returns(@token + '<=?')
J
Jeremy Kemper 已提交
416
    get :meta
417
    assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
418 419
    assert_select 'meta[name=?]', 'csrf-token'
    assert_match(/cf50faa3fe97702ca1ae&lt;=\?/, @response.body)
J
Jeremy Kemper 已提交
420
  end
421 422
end

423 424 425 426 427
class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
  class NullSessionDummyKeyGenerator
    def generate_key(secret)
      '03312270731a2ed0d11ed091c2338a06'
    end
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
  end

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

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

  test 'should allow to set encrypted cookies' do
    post :encrypted
    assert_response :ok
  end
443 444 445 446 447

  test 'should allow reset_session' do
    post :try_to_reset_session
    assert_response :ok
  end
448 449
end

450
class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
451 452 453 454 455 456 457 458
  include RequestForgeryProtectionTests
  def assert_blocked
    assert_raises(ActionController::InvalidAuthenticityToken) do
      yield
    end
  end
end

459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
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

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

494
class FreeCookieControllerTest < ActionController::TestCase
495 496 497 498
  def setup
    @controller = FreeCookieController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
J
Jeremy Kemper 已提交
499
    @token      = "cf50faa3fe97702ca1ae"
500

501
    SecureRandom.stubs(:base64).returns(@token)
502
  end
503

504 505 506 507
  def test_should_not_render_form_with_token_tag
    get :index
    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
  end
508

509 510 511 512
  def test_should_not_render_button_to_with_token_tag
    get :show_button
    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
  end
513

514
  def test_should_allow_all_methods_without_token
515
    [:post, :patch, :put, :delete].each do |method|
516 517 518
      assert_nothing_raised { send(method, :index)}
    end
  end
J
Jeremy Kemper 已提交
519 520 521

  test 'should not emit a csrf-token meta tag' do
    get :meta
522
    assert @response.body.blank?
J
Jeremy Kemper 已提交
523
  end
524
end
525 526 527

class CustomAuthenticityParamControllerTest < ActionController::TestCase
  def setup
528
    super
529 530
    @old_logger = ActionController::Base.logger
    @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
531
    @token = Base64.strict_encode64(SecureRandom.random_bytes(32))
Z
Zuhao Wan 已提交
532
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
533
    ActionController::Base.request_forgery_protection_token = @token
534 535 536
  end

  def teardown
Z
Zuhao Wan 已提交
537
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
538
    super
539 540
  end

541 542
  def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
    ActionController::Base.logger = @logger
543
    @controller.stubs(:valid_authenticity_token?).returns(:true)
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561

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

  def test_should_warn_if_form_authenticity_param_does_not_match_form_authenticity_token
    ActionController::Base.logger = @logger

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