request_forgery_protection_test.rb 16.2 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
  def unsafe
    render :text => 'pwn'
  end
18

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

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

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

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

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

57 58 59
end

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

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

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

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

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

  def try_to_reset_session
    reset_session
85
    head :ok
86
  end
87
end
88

89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
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

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

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

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

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

132
# common test methods
133
module RequestForgeryProtectionTests
134 135
  def setup
    @token      = "cf50faa3fe97702ca1ae"
136 137 138
    @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 已提交
139
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
140
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
E
Emilio Tagua 已提交
141 142
  end

143
  def teardown
Z
Zuhao Wan 已提交
144
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
145
  end
146

147 148 149 150
  def test_should_render_form_with_token_tag
    assert_not_blocked do
      get :index
    end
151
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
152 153
  end

E
Emilio Tagua 已提交
154
  def test_should_render_button_to_with_token_tag
155 156 157
    assert_not_blocked do
      get :show_button
    end
158
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
E
Emilio Tagua 已提交
159 160
  end

161
  def test_should_render_form_without_token_tag_if_remote
162 163 164
    assert_not_blocked do
      get :form_for_remote
    end
165
    assert_no_match(/authenticity_token/, response.body)
166 167
  end

168 169
  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
170
    begin
171
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
172 173 174
      assert_not_blocked do
        get :form_for_remote
      end
175
      assert_match(/authenticity_token/, response.body)
176
    ensure
177
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
178 179 180
    end
  end

181 182 183 184
  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
185
      assert_not_blocked do
186
        get :form_for_remote_with_external_token
187
      end
188
      assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
189
    ensure
190
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
191
    end
192 193
  end

194 195 196 197
  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
198
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
199 200
  end

201 202 203 204
  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
205
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
206 207
  end

208 209 210 211
  def test_should_render_form_with_token_tag_with_authenticity_token_requested
    assert_not_blocked do
      get :form_for_with_token
    end
212
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
213 214
  end

E
Emilio Tagua 已提交
215
  def test_should_allow_get
216
    assert_not_blocked { get :index }
E
Emilio Tagua 已提交
217 218
  end

219 220 221 222
  def test_should_allow_head
    assert_not_blocked { head :index }
  end

E
Emilio Tagua 已提交
223
  def test_should_allow_post_without_token_on_unsafe_action
224
    assert_not_blocked { post :unsafe }
225 226
  end

227 228
  def test_should_not_allow_post_without_token
    assert_blocked { post :index }
229 230
  end

231
  def test_should_not_allow_post_without_token_irrespective_of_format
232
    assert_blocked { post :index, format: 'xml' }
233 234
  end

235 236 237 238
  def test_should_not_allow_patch_without_token
    assert_blocked { patch :index }
  end

239 240
  def test_should_not_allow_put_without_token
    assert_blocked { put :index }
241
  end
242

243 244
  def test_should_not_allow_delete_without_token
    assert_blocked { delete :index }
245
  end
246

247
  def test_should_not_allow_xhr_post_without_token
248
    assert_blocked { post :index, xhr: true }
249
  end
250

251
  def test_should_allow_post_with_token
252
    assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
253
  end
254

255
  def test_should_allow_patch_with_token
256
    assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } }
257 258
  end

259
  def test_should_allow_put_with_token
260
    assert_not_blocked { put :index, params: { custom_authenticity_token: @token } }
261
  end
262

263
  def test_should_allow_delete_with_token
264
    assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } }
265
  end
266

267 268 269
  def test_should_allow_post_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { post :index }
270
  end
271

272 273 274
  def test_should_allow_delete_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { delete :index }
275
  end
276

277 278 279 280 281
  def test_should_allow_patch_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { patch :index }
  end

282 283 284
  def test_should_allow_put_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { put :index }
285
  end
286

287 288 289 290 291 292 293 294 295 296
  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 已提交
297
    ensure
298 299 300
      ActionController::Base.logger = old_logger
    end
  end
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316

  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
317

318 319 320 321 322 323 324 325
  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

326 327
    assert_cross_origin_not_blocked { get :same_origin_js, xhr: true }
    assert_cross_origin_not_blocked { get :same_origin_js, xhr: true, format: 'js'}
328 329
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
330
      get :negotiate_same_origin, xhr: true
331 332 333
    end
  end

334 335
  # Allow non-GET requests since GET is all a remote <script> tag can muster.
  def test_should_allow_non_get_js_without_xhr_header
336 337
    assert_cross_origin_not_blocked { post :same_origin_js, params: { custom_authenticity_token: @token } }
    assert_cross_origin_not_blocked { post :same_origin_js, params: { format: 'js', custom_authenticity_token: @token } }
338 339
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
340
      post :negotiate_same_origin, params: { custom_authenticity_token: @token}
341 342 343
    end
  end

344 345 346 347 348 349 350 351
  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

352 353
    assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true }
    assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true, format: 'js' }
354 355
    assert_cross_origin_not_blocked do
      @request.accept = 'text/javascript'
356
      get :negotiate_cross_origin, xhr: true
357 358 359
    end
  end

360 361 362 363 364 365 366
  def test_should_not_raise_error_if_token_is_not_a_string
    @controller.unstub(:valid_authenticity_token?)
    assert_blocked do
      patch :index, params: { custom_authenticity_token: { foo: 'bar' } }
    end
  end

367 368 369 370
  def assert_blocked
    session[:something_like_user_id] = 1
    yield
    assert_nil session[:something_like_user_id], "session values are still present"
371 372
    assert_response :success
  end
373

374 375
  def assert_not_blocked
    assert_nothing_raised { yield }
376 377
    assert_response :success
  end
378 379 380 381 382 383 384 385 386 387

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

  def assert_cross_origin_not_blocked
    assert_not_blocked { yield }
  end
388 389
end

390
# OK let's get our test on
391

392
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
393
  include RequestForgeryProtectionTests
J
Jeremy Kemper 已提交
394

395
  setup do
Z
Zuhao Wan 已提交
396
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
397 398 399 400
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
  end

  teardown do
Z
Zuhao Wan 已提交
401
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
402 403
  end

404
  test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
405
    @controller.stubs(:form_authenticity_token).returns(@token + '<=?')
J
Jeremy Kemper 已提交
406
    get :meta
407
    assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
408 409
    assert_select 'meta[name=?]', 'csrf-token'
    assert_match(/cf50faa3fe97702ca1ae&lt;=\?/, @response.body)
J
Jeremy Kemper 已提交
410
  end
411 412
end

413 414 415 416 417
class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
  class NullSessionDummyKeyGenerator
    def generate_key(secret)
      '03312270731a2ed0d11ed091c2338a06'
    end
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
  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
433 434 435 436 437

  test 'should allow reset_session' do
    post :try_to_reset_session
    assert_response :ok
  end
438 439
end

440
class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
441 442 443 444 445 446 447 448
  include RequestForgeryProtectionTests
  def assert_blocked
    assert_raises(ActionController::InvalidAuthenticityToken) do
      yield
    end
  end
end

449 450 451 452 453 454 455 456 457 458 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
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

484
class FreeCookieControllerTest < ActionController::TestCase
485 486
  def setup
    @controller = FreeCookieController.new
J
Jeremy Kemper 已提交
487
    @token      = "cf50faa3fe97702ca1ae"
488

489
    SecureRandom.stubs(:base64).returns(@token)
490
    super
491
  end
492

493 494 495 496
  def test_should_not_render_form_with_token_tag
    get :index
    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
  end
497

498 499 500 501
  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
502

503
  def test_should_allow_all_methods_without_token
504
    [:post, :patch, :put, :delete].each do |method|
505 506 507
      assert_nothing_raised { send(method, :index)}
    end
  end
J
Jeremy Kemper 已提交
508 509 510

  test 'should not emit a csrf-token meta tag' do
    get :meta
511
    assert @response.body.blank?
J
Jeremy Kemper 已提交
512
  end
513
end
514 515 516

class CustomAuthenticityParamControllerTest < ActionController::TestCase
  def setup
517
    super
518 519
    @old_logger = ActionController::Base.logger
    @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
520
    @token = Base64.strict_encode64(SecureRandom.random_bytes(32))
Z
Zuhao Wan 已提交
521
    @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
522
    ActionController::Base.request_forgery_protection_token = @token
523 524 525
  end

  def teardown
Z
Zuhao Wan 已提交
526
    ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
527
    super
528 529
  end

530 531
  def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
    ActionController::Base.logger = @logger
532
    @controller.stubs(:valid_authenticity_token?).returns(:true)
533 534

    begin
535
      post :index, params: { custom_token_name: 'foobar' }
536 537 538 539 540 541 542 543 544 545
      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
546
      post :index, params: { custom_token_name: 'bazqux' }
547 548 549 550
      assert_equal 1, @logger.logged(:warn).size
    ensure
      ActionController::Base.logger = @old_logger
    end
551 552
  end
end