request_forgery_protection_test.rb 14.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 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
class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
107
  self.allow_forgery_protection = false
108

109 110 111
  def index
    render :inline => "<%= form_tag('/') {} %>"
  end
112

113
  def show_button
114
    render :inline => "<%= button_to('New', '/') %>"
115 116 117
  end
end

118
class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession
119 120 121 122 123
  def form_authenticity_param
    'foobar'
  end
end

124
# common test methods
125
module RequestForgeryProtectionTests
126 127
  def setup
    @token      = "cf50faa3fe97702ca1ae"
128

129
    SecureRandom.stubs(:base64).returns(@token)
130
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
E
Emilio Tagua 已提交
131 132
  end

133 134 135
  def teardown
    ActionController::Base.request_forgery_protection_token = nil
  end
136

137 138 139 140
  def test_should_render_form_with_token_tag
    assert_not_blocked do
      get :index
    end
141
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
142 143
  end

E
Emilio Tagua 已提交
144
  def test_should_render_button_to_with_token_tag
145 146 147
    assert_not_blocked do
      get :show_button
    end
148
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
E
Emilio Tagua 已提交
149 150
  end

151
  def test_should_render_form_without_token_tag_if_remote
152 153 154
    assert_not_blocked do
      get :form_for_remote
    end
155
    assert_no_match(/authenticity_token/, response.body)
156 157
  end

158 159
  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
160
    begin
161
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
162 163 164
      assert_not_blocked do
        get :form_for_remote
      end
165
      assert_match(/authenticity_token/, response.body)
166
    ensure
167
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
168 169 170
    end
  end

171 172 173 174
  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
175
      assert_not_blocked do
176
        get :form_for_remote_with_external_token
177
      end
178
      assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
179
    ensure
180
      ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
181
    end
182 183
  end

184 185 186 187
  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
188
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
189 190
  end

191 192 193 194
  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
195
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
196 197
  end

198 199 200 201
  def test_should_render_form_with_token_tag_with_authenticity_token_requested
    assert_not_blocked do
      get :form_for_with_token
    end
202
    assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
203 204
  end

E
Emilio Tagua 已提交
205
  def test_should_allow_get
206
    assert_not_blocked { get :index }
E
Emilio Tagua 已提交
207 208
  end

209 210 211 212
  def test_should_allow_head
    assert_not_blocked { head :index }
  end

E
Emilio Tagua 已提交
213
  def test_should_allow_post_without_token_on_unsafe_action
214
    assert_not_blocked { post :unsafe }
215 216
  end

217 218
  def test_should_not_allow_post_without_token
    assert_blocked { post :index }
219 220
  end

221
  def test_should_not_allow_post_without_token_irrespective_of_format
222
    assert_blocked { post :index, format: 'xml' }
223 224
  end

225 226 227 228
  def test_should_not_allow_patch_without_token
    assert_blocked { patch :index }
  end

229 230
  def test_should_not_allow_put_without_token
    assert_blocked { put :index }
231
  end
232

233 234
  def test_should_not_allow_delete_without_token
    assert_blocked { delete :index }
235
  end
236

237 238
  def test_should_not_allow_xhr_post_without_token
    assert_blocked { xhr :post, :index }
239
  end
240

241
  def test_should_allow_post_with_token
242
    assert_not_blocked { post :index, :custom_authenticity_token => @token }
243
  end
244

245 246 247 248
  def test_should_allow_patch_with_token
    assert_not_blocked { patch :index, :custom_authenticity_token => @token }
  end

249
  def test_should_allow_put_with_token
250
    assert_not_blocked { put :index, :custom_authenticity_token => @token }
251
  end
252

253
  def test_should_allow_delete_with_token
254
    assert_not_blocked { delete :index, :custom_authenticity_token => @token }
255
  end
256

257 258 259
  def test_should_allow_post_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { post :index }
260
  end
261

262 263 264
  def test_should_allow_delete_with_token_in_header
    @request.env['HTTP_X_CSRF_TOKEN'] = @token
    assert_not_blocked { delete :index }
265
  end
266

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

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

277 278 279 280 281 282 283 284 285 286
  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 已提交
287
    ensure
288 289 290
      ActionController::Base.logger = old_logger
    end
  end
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306

  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
307

308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
  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

324 325 326 327 328 329 330 331 332 333
  # 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

334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
  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

350 351 352 353
  def assert_blocked
    session[:something_like_user_id] = 1
    yield
    assert_nil session[:something_like_user_id], "session values are still present"
354 355
    assert_response :success
  end
356

357 358
  def assert_not_blocked
    assert_nothing_raised { yield }
359 360
    assert_response :success
  end
361 362 363 364 365 366 367 368 369 370

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

  def assert_cross_origin_not_blocked
    assert_not_blocked { yield }
  end
371 372
end

373
# OK let's get our test on
374

375
class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
376
  include RequestForgeryProtectionTests
J
Jeremy Kemper 已提交
377

378 379 380 381 382 383 384 385
  setup do
    ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
  end

  teardown do
    ActionController::Base.request_forgery_protection_token = nil
  end

386
  test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
387
    SecureRandom.stubs(:base64).returns(@token + '<=?')
J
Jeremy Kemper 已提交
388
    get :meta
389
    assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
390
    assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae&lt;=?'
J
Jeremy Kemper 已提交
391
  end
392 393
end

394 395 396 397 398
class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
  class NullSessionDummyKeyGenerator
    def generate_key(secret)
      '03312270731a2ed0d11ed091c2338a06'
    end
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
  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
414 415 416 417 418

  test 'should allow reset_session' do
    post :try_to_reset_session
    assert_response :ok
  end
419 420
end

421
class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
422 423 424 425 426 427 428 429
  include RequestForgeryProtectionTests
  def assert_blocked
    assert_raises(ActionController::InvalidAuthenticityToken) do
      yield
    end
  end
end

430
class FreeCookieControllerTest < ActionController::TestCase
431 432 433 434
  def setup
    @controller = FreeCookieController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
J
Jeremy Kemper 已提交
435
    @token      = "cf50faa3fe97702ca1ae"
436

437
    SecureRandom.stubs(:base64).returns(@token)
438
  end
439

440 441 442 443
  def test_should_not_render_form_with_token_tag
    get :index
    assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
  end
444

445 446 447 448
  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
449

450
  def test_should_allow_all_methods_without_token
451
    [:post, :patch, :put, :delete].each do |method|
452 453 454
      assert_nothing_raised { send(method, :index)}
    end
  end
J
Jeremy Kemper 已提交
455 456 457

  test 'should not emit a csrf-token meta tag' do
    get :meta
458
    assert @response.body.blank?
J
Jeremy Kemper 已提交
459
  end
460
end
461 462 463

class CustomAuthenticityParamControllerTest < ActionController::TestCase
  def setup
464
    super
465 466 467 468
    @old_logger = ActionController::Base.logger
    @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
    @token = "foobar"
    ActionController::Base.request_forgery_protection_token = @token
469 470 471
  end

  def teardown
472
    ActionController::Base.request_forgery_protection_token = nil
473
    super
474 475
  end

476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
  def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
    ActionController::Base.logger = @logger
    SecureRandom.stubs(:base64).returns(@token)

    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
497 498
  end
end