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
@@ -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
|