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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +24 -0
  5. data/Appraisals +17 -0
  6. data/CHANGELOG.md +6 -0
  7. data/Gemfile +5 -0
  8. data/MIT-LICENSE +21 -0
  9. data/README.md +90 -0
  10. data/Rakefile +3 -0
  11. data/lib/unified_csrf_prevention/core.rb +48 -0
  12. data/lib/unified_csrf_prevention/middleware.rb +48 -0
  13. data/lib/unified_csrf_prevention/railtie.rb +22 -0
  14. data/lib/unified_csrf_prevention/request_forgery_protection.rb +68 -0
  15. data/lib/unified_csrf_prevention/version.rb +5 -0
  16. data/lib/unified_csrf_prevention.rb +8 -0
  17. data/spec/controllers/dummy_controller_spec.rb +211 -0
  18. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  19. data/spec/dummy/app/controllers/dummy_controller.rb +11 -0
  20. data/spec/dummy/app/views/dummy/index.html.erb +1 -0
  21. data/spec/dummy/app/views/layouts/application.html.erb +12 -0
  22. data/spec/dummy/config/application.rb +32 -0
  23. data/spec/dummy/config/boot.rb +3 -0
  24. data/spec/dummy/config/environment.rb +5 -0
  25. data/spec/dummy/config/environments/test.rb +44 -0
  26. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  27. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  28. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  29. data/spec/dummy/config/initializers/inflections.rb +16 -0
  30. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  31. data/spec/dummy/config/initializers/session_store.rb +3 -0
  32. data/spec/dummy/config/initializers/to_time_preserves_timezone.rb +10 -0
  33. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  34. data/spec/dummy/config/routes.rb +5 -0
  35. data/spec/dummy/config/secrets.yml +22 -0
  36. data/spec/dummy/config.ru +4 -0
  37. data/spec/dummy/log/.keep +0 -0
  38. data/spec/rails_helper.rb +9 -0
  39. data/spec/spec_helper.rb +22 -0
  40. data/spec/support/shared_context/for_controllers.rb +53 -0
  41. data/spec/support/shared_examples/for_controllers.rb +77 -0
  42. data/spec/support/shared_examples/for_middleware.rb +35 -0
  43. data/spec/xing/csrf_protection/core_spec.rb +100 -0
  44. data/spec/xing/csrf_protection/middleware_spec.rb +44 -0
  45. data/unified_csrf_prevention.gemspec +34 -0
  46. metadata +215 -0
@@ -0,0 +1,5 @@
1
+ require 'unified_csrf_prevention'
2
+
3
+ class ApplicationController < ActionController::Base
4
+ protect_from_forgery with: :exception
5
+ end
@@ -0,0 +1,11 @@
1
+ class DummyController < ApplicationController
2
+ def index; end
3
+
4
+ def success
5
+ head :ok
6
+ end
7
+
8
+ def update
9
+ head :ok
10
+ end
11
+ end
@@ -0,0 +1 @@
1
+ <%= form_tag %>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy App</title>
5
+ <%= csrf_meta_tags %>
6
+ </head>
7
+ <body>
8
+
9
+ <%= yield %>
10
+
11
+ </body>
12
+ </html>
@@ -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,3 @@
1
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
2
+
3
+ require 'bundler/setup' # Set up gems listed in the Gemfile.
@@ -0,0 +1,5 @@
1
+ # Load the Rails application.
2
+ require File.expand_path('../application', __FILE__)
3
+
4
+ # Initialize the Rails application.
5
+ Rails.application.initialize!
@@ -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,3 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ Rails.application.config.action_dispatch.cookies_serializer = :json
@@ -0,0 +1,4 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Configure sensitive parameters which will be filtered from the log file.
4
+ Rails.application.config.filter_parameters += [:password]
@@ -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,4 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Add new mime types for use in respond_to blocks:
4
+ # Mime::Type.register "text/richtext", :rtf
@@ -0,0 +1,3 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ Rails.application.config.session_store :disabled
@@ -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,5 @@
1
+ Rails.application.routes.draw do
2
+ get '/' => 'dummy#index'
3
+ post '/' => 'dummy#update'
4
+ get '/success' => 'dummy#success'
5
+ 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"] %>
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Rails.application
File without changes
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV['RAILS_ENV'] ||= 'test'
4
+
5
+ require 'spec_helper'
6
+ require File.expand_path('../dummy/config/environment', __FILE__)
7
+ require 'rspec/rails'
8
+
9
+ Dir[File.expand_path('**/*.rb', './spec/support/')].each { |f| require f }
@@ -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