unified_csrf_prevention 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
@@ -0,0 +1 @@
|
|
1
|
+
<%= form_tag %>
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path('../boot', __FILE__)
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
# Pick the frameworks you want:
|
5
|
+
# require "active_model/railtie"
|
6
|
+
# require "active_job/railtie"
|
7
|
+
# require "active_record/railtie"
|
8
|
+
require "action_controller/railtie"
|
9
|
+
# require "action_mailer/railtie"
|
10
|
+
require "action_view/railtie"
|
11
|
+
# require "sprockets/railtie"
|
12
|
+
# require "rails/test_unit/railtie"
|
13
|
+
|
14
|
+
# Require the gems listed in Gemfile, including any gems
|
15
|
+
# you've limited to :test, :development, or :production.
|
16
|
+
Bundler.require(*Rails.groups)
|
17
|
+
|
18
|
+
module Dummy
|
19
|
+
class Application < Rails::Application
|
20
|
+
# Settings in config/environments/* take precedence over those specified here.
|
21
|
+
# Application configuration should go into files in config/initializers
|
22
|
+
# -- all .rb files in that directory are automatically loaded.
|
23
|
+
|
24
|
+
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
25
|
+
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
26
|
+
# config.time_zone = 'Central Time (US & Canada)'
|
27
|
+
|
28
|
+
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
29
|
+
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
30
|
+
# config.i18n.default_locale = :de
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
Rails.application.configure do
|
2
|
+
# Settings specified here will take precedence over those in config/application.rb.
|
3
|
+
|
4
|
+
# The test environment is used exclusively to run your application's
|
5
|
+
# test suite. You never need to work with it otherwise. Remember that
|
6
|
+
# your test database is "scratch space" for the test suite and is wiped
|
7
|
+
# and recreated between test runs. Don't rely on the data there!
|
8
|
+
config.cache_classes = true
|
9
|
+
|
10
|
+
# Do not eager load code on boot. This avoids loading your whole application
|
11
|
+
# just for the purpose of running a single test. If you are using a tool that
|
12
|
+
# preloads Rails for running tests, you may have to set it to true.
|
13
|
+
config.eager_load = false
|
14
|
+
|
15
|
+
# Configure static file server for tests with Cache-Control for performance.
|
16
|
+
config.serve_static_files = true
|
17
|
+
config.static_cache_control = 'public, max-age=3600'
|
18
|
+
|
19
|
+
# Show full error reports and disable caching.
|
20
|
+
config.consider_all_requests_local = true
|
21
|
+
config.action_controller.perform_caching = false
|
22
|
+
|
23
|
+
# Raise exceptions instead of rendering exception templates.
|
24
|
+
config.action_dispatch.show_exceptions = false
|
25
|
+
|
26
|
+
# Enable request forgery protection in test environment.
|
27
|
+
config.action_controller.allow_forgery_protection = true
|
28
|
+
|
29
|
+
# Tell Action Mailer not to deliver emails to the real world.
|
30
|
+
# The :test delivery method accumulates sent emails in the
|
31
|
+
# ActionMailer::Base.deliveries array.
|
32
|
+
# config.action_mailer.delivery_method = :test
|
33
|
+
|
34
|
+
# Randomize the order test cases are executed.
|
35
|
+
config.active_support.test_order = :random
|
36
|
+
|
37
|
+
# Print deprecation notices to the stderr.
|
38
|
+
config.active_support.deprecation = :stderr
|
39
|
+
|
40
|
+
# Raises error for missing translations
|
41
|
+
# config.action_view.raise_on_missing_translations = true
|
42
|
+
|
43
|
+
config.unified_csrf_prevention_key = 'protected to the max'
|
44
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
|
4
|
+
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
|
5
|
+
|
6
|
+
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
|
7
|
+
# Rails.backtrace_cleaner.remove_silencers!
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# Add new inflection rules using the following format. Inflections
|
4
|
+
# are locale specific, and you may define rules for as many different
|
5
|
+
# locales as you wish. All of these examples are active by default:
|
6
|
+
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
7
|
+
# inflect.plural /^(ox)$/i, '\1en'
|
8
|
+
# inflect.singular /^(ox)en/i, '\1'
|
9
|
+
# inflect.irregular 'person', 'people'
|
10
|
+
# inflect.uncountable %w( fish sheep )
|
11
|
+
# end
|
12
|
+
|
13
|
+
# These inflection rules are supported but not enabled by default:
|
14
|
+
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
15
|
+
# inflect.acronym 'RESTful'
|
16
|
+
# end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# Preserve the timezone of the receiver when calling to `to_time`.
|
4
|
+
# Ruby 2.4 will change the behavior of `to_time` to preserve the timezone
|
5
|
+
# when converting to an instance of `Time` instead of the previous behavior
|
6
|
+
# of converting to the local system timezone.
|
7
|
+
#
|
8
|
+
# Rails 5.0 introduced this config option so that apps made with earlier
|
9
|
+
# versions of Rails are not affected when upgrading.
|
10
|
+
ActiveSupport.to_time_preserves_timezone = true
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# This file contains settings for ActionController::ParamsWrapper which
|
4
|
+
# is enabled by default.
|
5
|
+
|
6
|
+
# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
|
7
|
+
ActiveSupport.on_load(:action_controller) do
|
8
|
+
wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
|
9
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# Your secret key is used for verifying the integrity of signed cookies.
|
4
|
+
# If you change this key, all old signed cookies will become invalid!
|
5
|
+
|
6
|
+
# Make sure the secret is at least 30 characters and all random,
|
7
|
+
# no regular words or you'll be exposed to dictionary attacks.
|
8
|
+
# You can use `rake secret` to generate a secure secret key.
|
9
|
+
|
10
|
+
# Make sure the secrets in this file are kept private
|
11
|
+
# if you're sharing your code publicly.
|
12
|
+
|
13
|
+
development:
|
14
|
+
secret_key_base: e20fedf9cf9b2c879b4865d7b0c9485e262ddb5fe169b9b8fcae0ada5c1a7525e0f9a41e80fbb6a44c6f9ea367199d0671d187ae3ffea2199f47e41a2593360b
|
15
|
+
|
16
|
+
test:
|
17
|
+
secret_key_base: 3683f82c161cabc21d5d60e3b23aa6ec3b74b4601d0ea0dbc35baf027c8eaee0194d24391954cd5cba71cafaa8f39820c267bf827117dd21c753ca43058113c6
|
18
|
+
|
19
|
+
# Do not keep production secrets in the repository,
|
20
|
+
# instead read values from the environment.
|
21
|
+
production:
|
22
|
+
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
|
File without changes
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'unified_csrf_prevention'
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.expect_with :rspec do |expectations|
|
7
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
8
|
+
end
|
9
|
+
|
10
|
+
config.mock_with :rspec do |mocks|
|
11
|
+
mocks.verify_partial_doubles = true
|
12
|
+
end
|
13
|
+
|
14
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
15
|
+
|
16
|
+
config.filter_run_when_matching :focus
|
17
|
+
|
18
|
+
config.warnings = true
|
19
|
+
|
20
|
+
config.order = :random
|
21
|
+
Kernel.srand config.seed
|
22
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
shared_context 'with token and checksum' do
|
4
|
+
let(:token) { 'A token should be 32 bytes long.' }
|
5
|
+
let(:checksum) { 'a checksum' }
|
6
|
+
|
7
|
+
before do
|
8
|
+
allow(UnifiedCsrfPrevention::Core).to receive(:valid_token?).and_return(false)
|
9
|
+
allow(UnifiedCsrfPrevention::Core).to receive(:valid_token?).with(token, checksum).and_return(valid_token?)
|
10
|
+
|
11
|
+
request.cookies['csrf_token'] = token
|
12
|
+
request.cookies['csrf_checksum'] = checksum
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
shared_context 'empty update request' do
|
17
|
+
let(:perform_request) do
|
18
|
+
post :update
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
shared_context 'update request with a csrf token form parameter' do
|
23
|
+
let(:perform_request) do
|
24
|
+
post_with_params :update, params: { authenticity_token: token }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
shared_context 'update request with a fetched csrf token form parameter' do
|
29
|
+
render_views
|
30
|
+
|
31
|
+
let(:perform_request) do
|
32
|
+
get :index
|
33
|
+
actual_token = response.body.match(%r{<input type="hidden" name="authenticity_token" value="([^"]+)" />})[1]
|
34
|
+
|
35
|
+
post_with_params :update, params: { authenticity_token: actual_token }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
shared_context 'update request with a csrf token header' do
|
40
|
+
let(:perform_request) do
|
41
|
+
request.headers['X-CSRF-Token'] = token
|
42
|
+
post :update
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# rspec-rails `post` interface is different starting from Rails 5
|
47
|
+
# https://relishapp.com/rspec/rspec-rails/docs/request-specs/request-spec#specify-managing-a-widget-with-rails-integration-methods
|
48
|
+
|
49
|
+
POST_RAILS_5 = Gem.loaded_specs['rails']&.version.to_s.to_i >= 5
|
50
|
+
|
51
|
+
def post_with_params(action, params:)
|
52
|
+
post action, POST_RAILS_5 ? { params: params } : params
|
53
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
TOKEN_INPUT_REGEXP = %r{<input type="hidden" name="authenticity_token" value="([^"]+)" />}
|
4
|
+
TOKEN_META_REGEXP = %r{<meta name="csrf-token" content="([^"]+)" />}
|
5
|
+
|
6
|
+
shared_examples 'an action that outputs the csrf token' do
|
7
|
+
let(:real_form_token) do
|
8
|
+
encoded_token = response.body.match(TOKEN_INPUT_REGEXP)[1]
|
9
|
+
decode_token(encoded_token)
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:real_meta_token) do
|
13
|
+
encoded_token = response.body.match(TOKEN_META_REGEXP)[1]
|
14
|
+
decode_token(encoded_token)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'renders form with a new token' do
|
18
|
+
perform_request
|
19
|
+
|
20
|
+
expect(response.body).to match(TOKEN_INPUT_REGEXP)
|
21
|
+
expect(real_form_token).to eq(output_token)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'renders csrf meta tags with a new token' do
|
25
|
+
perform_request
|
26
|
+
|
27
|
+
expect(response.body).to match(%r{<meta name="csrf-param" content="authenticity_token" />})
|
28
|
+
expect(response.body).to match(TOKEN_META_REGEXP)
|
29
|
+
expect(real_meta_token).to eq(output_token)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
shared_examples 'an action that outputs the csrf cookies' do
|
34
|
+
it 'sets the token for the middleware' do
|
35
|
+
begin
|
36
|
+
perform_request
|
37
|
+
rescue ActionController::InvalidAuthenticityToken
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
expect(request.env).to include('unified_csrf_prevention.token' => output_token)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
shared_examples 'an action that does not output the csrf cookies' do
|
46
|
+
it 'does not set the token for the middleware' do
|
47
|
+
begin
|
48
|
+
perform_request
|
49
|
+
rescue ActionController::InvalidAuthenticityToken
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
expect(request.env).not_to include('unified_csrf_prevention.token')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
shared_examples 'an action that responds with a csrf validation error' do
|
58
|
+
it do
|
59
|
+
expect { perform_request }.to raise_error ActionController::InvalidAuthenticityToken
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
shared_examples 'an action that responds with OK' do
|
64
|
+
it do
|
65
|
+
perform_request
|
66
|
+
|
67
|
+
expect(response).to be_ok
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Code parts were taken from ActionController::RequestForgeryProtection
|
72
|
+
def decode_token(encoded_masked_token)
|
73
|
+
masked_token = Base64.strict_decode64(encoded_masked_token)
|
74
|
+
one_time_pad = masked_token[0...ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH]
|
75
|
+
encrypted_csrf_token = masked_token[ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH..-1]
|
76
|
+
one_time_pad.bytes.zip(encrypted_csrf_token.bytes).map { |(c1, c2)| c1 ^ c2 }.pack('c*')
|
77
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
shared_examples 'a middleware that does not set any cookie' do
|
4
|
+
it { is_expected.to be_ok }
|
5
|
+
|
6
|
+
it 'does not output Set-Cookie header' do
|
7
|
+
expect(subject.headers).not_to include('Set-Cookie')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
shared_examples 'a middleware that sets secure csrf cookies' do
|
12
|
+
it { is_expected.to be_ok }
|
13
|
+
it 'sets the csrf cookie headers' do
|
14
|
+
expect(subject.headers).to include('Set-Cookie' => %r{^csrf_token=#{token}; path=/; secure; SameSite=Strict$})
|
15
|
+
expect(subject.headers).to include('Set-Cookie' => %r{^csrf_checksum=#{checksum}; path=/; secure; HttpOnly; SameSite=Strict$})
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
shared_examples 'a middleware that logs generated csrf token' do
|
20
|
+
before { allow(Rails.logger).to receive(:info) }
|
21
|
+
|
22
|
+
it { is_expected.to be_ok }
|
23
|
+
it 'logs the token' do
|
24
|
+
subject
|
25
|
+
expect(Rails.logger).to have_received(:info).with("Set CSRF token: #{token}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
shared_examples 'a middleware that sets insecure csrf cookies' do
|
30
|
+
it { is_expected.to be_ok }
|
31
|
+
it 'sets the csrf cookie headers' do
|
32
|
+
expect(subject.headers).to include('Set-Cookie' => %r{^csrf_token=#{token}; path=/; SameSite=Strict$})
|
33
|
+
expect(subject.headers).to include('Set-Cookie' => %r{^csrf_checksum=#{checksum}; path=/; HttpOnly; SameSite=Strict$})
|
34
|
+
end
|
35
|
+
end
|