unified_csrf_prevention 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rubocop.yml +24 -0
- data/Appraisals +17 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +5 -0
- data/MIT-LICENSE +21 -0
- data/README.md +90 -0
- data/Rakefile +3 -0
- data/lib/unified_csrf_prevention/core.rb +48 -0
- data/lib/unified_csrf_prevention/middleware.rb +48 -0
- data/lib/unified_csrf_prevention/railtie.rb +22 -0
- data/lib/unified_csrf_prevention/request_forgery_protection.rb +68 -0
- data/lib/unified_csrf_prevention/version.rb +5 -0
- data/lib/unified_csrf_prevention.rb +8 -0
- data/spec/controllers/dummy_controller_spec.rb +211 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/dummy_controller.rb +11 -0
- data/spec/dummy/app/views/dummy/index.html.erb +1 -0
- data/spec/dummy/app/views/layouts/application.html.erb +12 -0
- data/spec/dummy/config/application.rb +32 -0
- data/spec/dummy/config/boot.rb +3 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/test.rb +44 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/to_time_preserves_timezone.rb +10 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/rails_helper.rb +9 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/shared_context/for_controllers.rb +53 -0
- data/spec/support/shared_examples/for_controllers.rb +77 -0
- data/spec/support/shared_examples/for_middleware.rb +35 -0
- data/spec/xing/csrf_protection/core_spec.rb +100 -0
- data/spec/xing/csrf_protection/middleware_spec.rb +44 -0
- data/unified_csrf_prevention.gemspec +34 -0
- metadata +215 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: efb44eb12723e4bc987a8480023d81806f5802ef
|
4
|
+
data.tar.gz: b77f535be345db63c4949fa3ede96d9c52f11768
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dec3d84b3db6f629fbb25049e7bbab44fdf769bfad03352c504967c7cba4e76e9b47de0a46af5bd0b968e6fde33932c2002d6a3c9f3e77dda66ce06d3ce33dc7
|
7
|
+
data.tar.gz: c72c8c82e6740c1d239da46e1320d83b8a5b3092e8ed0271a444525a375f52106b3db77167295967a8f4f2871717a9b939f98a67e09c8a74a26feb60d7c9c800
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.3
|
3
|
+
Exclude:
|
4
|
+
- 'spec/dummy/**/*'
|
5
|
+
- 'gems/**/*'
|
6
|
+
|
7
|
+
Metrics/LineLength:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Metrics/BlockLength:
|
11
|
+
Exclude:
|
12
|
+
- 'spec/**/*.rb'
|
13
|
+
|
14
|
+
Layout/IndentationWidth:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Layout/ElseAlignment:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Style/TrailingCommaInLiteral:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Lint/EndAlignment:
|
24
|
+
Enabled: false
|
data/Appraisals
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
appraise 'rails-4.2' do
|
4
|
+
gem 'rails', '~>4.2'
|
5
|
+
end
|
6
|
+
|
7
|
+
appraise 'rails-5.0' do
|
8
|
+
gem 'rails', '~>5.0'
|
9
|
+
end
|
10
|
+
|
11
|
+
appraise 'rails-5.1' do
|
12
|
+
gem 'rails', '~>5.1'
|
13
|
+
end
|
14
|
+
|
15
|
+
appraise 'rails-5.2' do
|
16
|
+
gem 'rails', '~>5.2'
|
17
|
+
end
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017-2018 XING SE
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Unified CSRF Protection for Rails
|
2
|
+
|
3
|
+
This gem is a drop-in upgrade for request forgery protection in Rails 4 and 5 with the following benefits:
|
4
|
+
|
5
|
+
* It is self-healing by design. Whenever a user comes in with an invalid authenticity token, they will have a valid one sent back along with the error response so the next request will succeed. There's no need for the user to reload the page.
|
6
|
+
* The CSRF protection is decoupled from the users' sessions that could eventually get wiped out or overwritten, resulting in errors.
|
7
|
+
* The approach is framework/language agnostic, so the token generated by one application will be accepted by any other, including non-Rails applications (given they implement the same CSRF protection scheme).
|
8
|
+
* The mechanism is stateless from the backend's perspective. The data is stored in the browser's cookies, requiring no backend storage.
|
9
|
+
* The solution design was audited by Xing Security team and [cure53](https://cure53.de).
|
10
|
+
* It is both React- and jQuery-friendly.
|
11
|
+
|
12
|
+
Please read the [Cross-application CSRF Prevention specification](https://github.com/xing/cross-application-csrf-prevention) for design and implementation details.
|
13
|
+
|
14
|
+
## Configuring Your Application
|
15
|
+
|
16
|
+
1. Add the gem to the `Gemfile`:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'unified_csrf_prevention'
|
20
|
+
```
|
21
|
+
|
22
|
+
2. Set the shared secret key configuration value for `production`, `preview` and whatever other environment your have:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
Rails.application.configure do
|
26
|
+
# existing configuration settings
|
27
|
+
|
28
|
+
config.unified_csrf_prevention_key = '64 random characters'
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
3. Replace the unobtrusive scripting adapter that adds the `X-CSRF-Token` header which comes with Rails with this one (assuming jQuery is used):
|
33
|
+
|
34
|
+
```js
|
35
|
+
$.ajaxPrefilter(function(options) {
|
36
|
+
var token = (document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/) || [])[1];
|
37
|
+
|
38
|
+
if (token) {
|
39
|
+
options.headers["X-CSRF-Token"] = token;
|
40
|
+
}
|
41
|
+
});
|
42
|
+
```
|
43
|
+
|
44
|
+
Important note: **the token must be read from cookies for each and every frontend request the application makes. It is not acceptable to read the token once and store it in some variable, DOM node or in any other form.**
|
45
|
+
|
46
|
+
In other words, please do exactly what the provided snippet does - for any request read the token from the cookie right before the request is sent. Don't try to cache the token, transfer it from the backend, or optimize out the cookie access, otherwise your application could end up using invalid tokens.
|
47
|
+
|
48
|
+
If your application uses something different from jQuery to make AJAX calls, please adjust the snippet accordingly. The key parts are running the code before each request is sent, and setting the header with the value read from the cookie. Basically, `$.ajaxPrefilter` and `options.headers` should be replaced with something that works with the library you use instead of jQuery.
|
49
|
+
|
50
|
+
If your application for some reason has several different ways to send AJAX requests, you need to adjust all of them.
|
51
|
+
|
52
|
+
## Usage
|
53
|
+
|
54
|
+
The gem is seamlessly integrated with Rails' built-in request forgery protection mechanism so there's nothing special to be done on top of the regular `protect_from_forgery` controller setting.
|
55
|
+
Authenticity tokens transferred in hidden inputs as well as per-form authenticity tokens introduced in Rails 5 just work out of the box.
|
56
|
+
See [Ruby on Rails Security Guide](http://guides.rubyonrails.org/security.html#csrf-countermeasures) for details.
|
57
|
+
|
58
|
+
## Testing Controllers with Forgery Protection Enabled
|
59
|
+
|
60
|
+
Sometimes it's necessary to test the controller code with the actual forgery protection mechanisms enabled (`allow_forgery_protection` overwritten in tests).
|
61
|
+
Providing the cookies for requests to make `unified_csrf_prevention` work is a bit of a hassle, so instead it's possible to mock the token validation and thus make the controller accept the supplied token:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
describe '#some_action' do
|
65
|
+
context 'when requested with valid csrf token' do
|
66
|
+
let(:csrf_token) { controller.send(:form_authenticity_token) }
|
67
|
+
|
68
|
+
before do
|
69
|
+
allow(controller).to receive(:valid_token?).with(csrf_token).and_return true
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'executes action' do
|
73
|
+
post :some_action, authenticity_token: csrf_token
|
74
|
+
expect(response).to be_ok
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
## Compatibility
|
81
|
+
|
82
|
+
The gem is compatible with Rails 4.2, 5.0, 5.1 and 5.2.
|
83
|
+
|
84
|
+
## Running Specs
|
85
|
+
|
86
|
+
```bash
|
87
|
+
rubocop
|
88
|
+
appraisal install
|
89
|
+
appraisal rspec
|
90
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'openssl'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
require 'rails'
|
8
|
+
require 'active_support/security_utils'
|
9
|
+
|
10
|
+
module UnifiedCsrfPrevention
|
11
|
+
# Low-level routines and constants
|
12
|
+
# See https://github.com/xing/cross-application-csrf-prevention#low-level-implementation-details
|
13
|
+
module Core
|
14
|
+
TOKEN_COOKIE_NAME = 'csrf_token'
|
15
|
+
CHECKSUM_COOKIE_NAME = 'csrf_checksum'
|
16
|
+
TOKEN_RACK_ENV_VAR = 'unified_csrf_prevention.token'
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def generate_token
|
20
|
+
random_bytes_needed = (ActionController::Base::AUTHENTICITY_TOKEN_LENGTH * 0.75).ceil # Base 64 requires four bytes to store three bytes of data
|
21
|
+
random_bytes = SecureRandom.random_bytes(random_bytes_needed)
|
22
|
+
encode(random_bytes)[0...ActionController::Base::AUTHENTICITY_TOKEN_LENGTH]
|
23
|
+
end
|
24
|
+
|
25
|
+
def checksum_for(token)
|
26
|
+
digest_algorithm = OpenSSL::Digest::SHA256.new
|
27
|
+
token_digest = OpenSSL::HMAC.digest(digest_algorithm, shared_secret_key, token)
|
28
|
+
encode(token_digest)
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_token?(token, checksum)
|
32
|
+
!token.nil? && !checksum.nil? && ActiveSupport::SecurityUtils.secure_compare(checksum_for(token), checksum)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def shared_secret_key
|
38
|
+
Rails.configuration.unified_csrf_prevention_key
|
39
|
+
rescue NoMethodError
|
40
|
+
raise UnifiedCsrfPrevention::ConfigurationError, 'Configuration setting `unified_csrf_prevention_key` is not defined'
|
41
|
+
end
|
42
|
+
|
43
|
+
def encode(binary_string)
|
44
|
+
Base64.urlsafe_encode64(binary_string, padding: false)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'unified_csrf_prevention/core'
|
4
|
+
|
5
|
+
module UnifiedCsrfPrevention
|
6
|
+
# Rack middleware to set the token and checksum cookies
|
7
|
+
# See https://github.com/xing/cross-application-csrf-prevention#token-generation
|
8
|
+
class Middleware
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
status, headers, body = @app.call(env)
|
15
|
+
|
16
|
+
if env.key?(Core::TOKEN_RACK_ENV_VAR)
|
17
|
+
token = env[Core::TOKEN_RACK_ENV_VAR]
|
18
|
+
set_csrf_cookies!(headers, token)
|
19
|
+
Rails.logger.info("Set CSRF token: #{token}")
|
20
|
+
end
|
21
|
+
|
22
|
+
[status, headers, body]
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def set_csrf_cookies!(headers, token)
|
28
|
+
checksum = Core.checksum_for(token)
|
29
|
+
|
30
|
+
set_cookie!(headers, Core::TOKEN_COOKIE_NAME, value: token)
|
31
|
+
set_cookie!(headers, Core::CHECKSUM_COOKIE_NAME, value: checksum, httponly: true)
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_cookie!(headers, name, data)
|
35
|
+
cookie = {
|
36
|
+
path: '/',
|
37
|
+
secure: secure_cookies?,
|
38
|
+
same_site: :strict,
|
39
|
+
}.merge(data)
|
40
|
+
|
41
|
+
Rack::Utils.set_cookie_header!(headers, name, cookie)
|
42
|
+
end
|
43
|
+
|
44
|
+
def secure_cookies?
|
45
|
+
Rails.env.production? || Rails.env.preview?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/railtie'
|
4
|
+
|
5
|
+
require 'unified_csrf_prevention/middleware'
|
6
|
+
require 'unified_csrf_prevention/request_forgery_protection'
|
7
|
+
|
8
|
+
module UnifiedCsrfPrevention
|
9
|
+
# A Railtie to automagically set up the gem's middleware and include
|
10
|
+
# the controller concern when the gem is loaded
|
11
|
+
class Railtie < Rails::Railtie
|
12
|
+
initializer :unified_csrf_prevention_middleware do |app|
|
13
|
+
app.middleware.use UnifiedCsrfPrevention::Middleware
|
14
|
+
end
|
15
|
+
|
16
|
+
initializer :unified_csrf_prevention_concern do
|
17
|
+
ActiveSupport.on_load :action_controller do
|
18
|
+
include UnifiedCsrfPrevention::RequestForgeryProtection
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
require 'unified_csrf_prevention/core'
|
6
|
+
require 'unified_csrf_prevention/middleware'
|
7
|
+
|
8
|
+
module UnifiedCsrfPrevention
|
9
|
+
# ApplicationController concern implementing request authenticity validation
|
10
|
+
# See https://github.com/xing/cross-application-csrf-prevention#application-action-filter
|
11
|
+
module RequestForgeryProtection
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
def protect_from_forgery(options = {})
|
16
|
+
super
|
17
|
+
prepend_before_action :setup_csrf_token
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def valid_authenticity_token?(_session, token)
|
24
|
+
valid_token?(token) || super
|
25
|
+
end
|
26
|
+
|
27
|
+
def compare_with_real_token(token, _session)
|
28
|
+
valid_token?(token)
|
29
|
+
end
|
30
|
+
|
31
|
+
def real_csrf_token(_session)
|
32
|
+
csrf_token
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup_csrf_token
|
36
|
+
csrf_token
|
37
|
+
end
|
38
|
+
|
39
|
+
def csrf_token
|
40
|
+
@csrf_token ||= if valid_token?(existing_token) && token_of_correct_length?(existing_token)
|
41
|
+
existing_token
|
42
|
+
else
|
43
|
+
new_token
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def new_token
|
48
|
+
raise UnifiedCsrfPrevention::ConfigurationError, 'UnifiedCsrfPrevention::Middleware middleware must be used' unless Rails.configuration.middleware.include?(UnifiedCsrfPrevention::Middleware)
|
49
|
+
request.env[Core::TOKEN_RACK_ENV_VAR] = Core.generate_token
|
50
|
+
end
|
51
|
+
|
52
|
+
def valid_token?(token)
|
53
|
+
Core.valid_token?(token, checksum)
|
54
|
+
end
|
55
|
+
|
56
|
+
def existing_token
|
57
|
+
request.cookies[Core::TOKEN_COOKIE_NAME]
|
58
|
+
end
|
59
|
+
|
60
|
+
def checksum
|
61
|
+
request.cookies[Core::CHECKSUM_COOKIE_NAME]
|
62
|
+
end
|
63
|
+
|
64
|
+
def token_of_correct_length?(token)
|
65
|
+
token.length == ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
describe DummyController, type: :controller do
|
6
|
+
let(:new_token) { 'This should be exactly 32 bytes.' }
|
7
|
+
let(:short_token) { '😻' }
|
8
|
+
|
9
|
+
before do
|
10
|
+
ActionController::Base.allow_forgery_protection = true # RSpec unconditionally disallows the forgery protection in controller tests
|
11
|
+
allow(UnifiedCsrfPrevention::Core).to receive(:generate_token).and_return(new_token)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#index' do
|
15
|
+
render_views
|
16
|
+
|
17
|
+
let(:perform_request) do
|
18
|
+
get :index
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when token and checksum cookies not sent' do
|
22
|
+
let(:output_token) { new_token }
|
23
|
+
|
24
|
+
it_behaves_like 'an action that outputs the csrf token'
|
25
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when token and checksum cookies sent' do
|
29
|
+
include_context 'with token and checksum'
|
30
|
+
|
31
|
+
context 'with a valid token' do
|
32
|
+
let(:valid_token?) { true }
|
33
|
+
let(:output_token) { token }
|
34
|
+
|
35
|
+
it_behaves_like 'an action that outputs the csrf token'
|
36
|
+
it_behaves_like 'an action that does not output the csrf cookies'
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'with a valid but short token' do
|
40
|
+
let(:valid_token?) { true }
|
41
|
+
let(:token) { short_token }
|
42
|
+
let(:output_token) { new_token }
|
43
|
+
|
44
|
+
it_behaves_like 'an action that outputs the csrf token'
|
45
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'with an invalid token' do
|
49
|
+
let(:valid_token?) { false }
|
50
|
+
let(:output_token) { new_token }
|
51
|
+
|
52
|
+
it_behaves_like 'an action that outputs the csrf token'
|
53
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when cookies middleware is not installed' do
|
58
|
+
before do
|
59
|
+
allow(Rails.application.config.middleware).to receive(:include?).with(UnifiedCsrfPrevention::Middleware).and_return(false)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'raises a configuration error' do
|
63
|
+
expect { perform_request }.to raise_error UnifiedCsrfPrevention::ConfigurationError
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#success' do
|
69
|
+
let(:perform_request) do
|
70
|
+
get :success
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'when token and checksum cookies not sent' do
|
74
|
+
let(:output_token) { new_token }
|
75
|
+
|
76
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'when token and checksum cookies sent' do
|
80
|
+
include_context 'with token and checksum'
|
81
|
+
|
82
|
+
context 'with a valid token' do
|
83
|
+
let(:valid_token?) { true }
|
84
|
+
let(:output_token) { token }
|
85
|
+
|
86
|
+
it_behaves_like 'an action that does not output the csrf cookies'
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'with an invalid token' do
|
90
|
+
let(:valid_token?) { false }
|
91
|
+
let(:output_token) { new_token }
|
92
|
+
|
93
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#update' do
|
99
|
+
context 'when token and checksum cookies not sent' do
|
100
|
+
let(:output_token) { new_token }
|
101
|
+
|
102
|
+
context 'request has no parameters' do
|
103
|
+
include_context 'empty update request'
|
104
|
+
|
105
|
+
it_behaves_like 'an action that responds with a csrf validation error'
|
106
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'request includes a csrf token form parameter' do
|
110
|
+
let(:token) { 'some token out of nowhere' }
|
111
|
+
include_context 'update request with a csrf token form parameter'
|
112
|
+
|
113
|
+
it_behaves_like 'an action that responds with a csrf validation error'
|
114
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'request includes a csrf token header' do
|
118
|
+
let(:token) { 'some token out of nowhere' }
|
119
|
+
include_context 'update request with a csrf token header'
|
120
|
+
|
121
|
+
it_behaves_like 'an action that responds with a csrf validation error'
|
122
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context 'when token and checksum cookies sent' do
|
127
|
+
include_context 'with token and checksum'
|
128
|
+
|
129
|
+
context 'with a valid token' do
|
130
|
+
let(:valid_token?) { true }
|
131
|
+
|
132
|
+
context 'and token is of shorter length' do
|
133
|
+
let(:token) { short_token }
|
134
|
+
let(:output_token) { new_token }
|
135
|
+
|
136
|
+
include_context 'update request with a csrf token header'
|
137
|
+
|
138
|
+
it_behaves_like 'an action that responds with OK'
|
139
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'request has no parameters' do
|
143
|
+
include_context 'empty update request'
|
144
|
+
|
145
|
+
it_behaves_like 'an action that responds with a csrf validation error'
|
146
|
+
it_behaves_like 'an action that does not output the csrf cookies'
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'request includes a csrf token form parameter' do
|
150
|
+
include_context 'update request with a csrf token form parameter'
|
151
|
+
|
152
|
+
it_behaves_like 'an action that responds with OK'
|
153
|
+
it_behaves_like 'an action that does not output the csrf cookies'
|
154
|
+
end
|
155
|
+
|
156
|
+
context 'request includes a csrf token form parameter which was generated on the backend' do
|
157
|
+
include_context 'update request with a fetched csrf token form parameter'
|
158
|
+
|
159
|
+
it_behaves_like 'an action that responds with OK'
|
160
|
+
it_behaves_like 'an action that does not output the csrf cookies'
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'request includes a csrf token header' do
|
164
|
+
include_context 'update request with a csrf token header'
|
165
|
+
|
166
|
+
it_behaves_like 'an action that responds with OK'
|
167
|
+
it_behaves_like 'an action that does not output the csrf cookies'
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context 'with an invalid token' do
|
172
|
+
let(:valid_token?) { false }
|
173
|
+
let(:output_token) { new_token }
|
174
|
+
|
175
|
+
context 'request has no parameters' do
|
176
|
+
include_context 'empty update request'
|
177
|
+
|
178
|
+
it_behaves_like 'an action that responds with a csrf validation error'
|
179
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
180
|
+
end
|
181
|
+
|
182
|
+
context 'request includes a csrf form parameter' do
|
183
|
+
include_context 'update request with a csrf token form parameter'
|
184
|
+
|
185
|
+
it_behaves_like 'an action that responds with a csrf validation error'
|
186
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
187
|
+
end
|
188
|
+
|
189
|
+
context 'request includes a csrf token header' do
|
190
|
+
include_context 'update request with a csrf token header'
|
191
|
+
|
192
|
+
it_behaves_like 'an action that responds with a csrf validation error'
|
193
|
+
it_behaves_like 'an action that outputs the csrf cookies'
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
describe 'on_load hook' do
|
200
|
+
context 'is triggered twice' do
|
201
|
+
before do
|
202
|
+
ActiveSupport.run_load_hooks(:action_controller, ActionController::Base)
|
203
|
+
ActiveSupport.run_load_hooks(:action_controller, ActionController::Base)
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'includes RequestForgeryProtection only ones' do
|
207
|
+
expect(ActionController::Base.included_modules.find_all { |m| m == UnifiedCsrfPrevention::RequestForgeryProtection }.length).to be 1
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|