├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .rubocop.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── rake ├── lib └── omniauth │ ├── rails_csrf_protection.rb │ └── rails_csrf_protection │ ├── railtie.rb │ ├── token_verifier.rb │ └── version.rb ├── omniauth-rails_csrf_protection.gemspec └── test ├── application_test.rb └── test_helper.rb /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | include: 11 | - ruby: "2.4" 12 | rails: ~> 4.2.0 13 | bundler: 1.17.3 14 | - ruby: "2.4" 15 | rails: ~> 5.0.0 16 | - ruby: "2.4" 17 | rails: ~> 5.1.0 18 | - ruby: "2.4" 19 | rails: ~> 5.2.0 20 | 21 | - ruby: "2.5" 22 | rails: ~> 5.0.0 23 | - ruby: "2.5" 24 | rails: ~> 5.1.0 25 | - ruby: "2.5" 26 | rails: ~> 5.2.0 27 | - ruby: "2.5" 28 | rails: ~> 6.0.0 29 | - ruby: "2.5" 30 | rails: ~> 6.1.0 31 | 32 | - ruby: "2.6" 33 | rails: ~> 5.0.0 34 | - ruby: "2.6" 35 | rails: ~> 5.1.0 36 | - ruby: "2.6" 37 | rails: ~> 5.2.0 38 | - ruby: "2.6" 39 | rails: ~> 6.0.0 40 | - ruby: "2.6" 41 | rails: ~> 6.1.0 42 | 43 | - ruby: "2.7" 44 | rails: ~> 5.0.0 45 | - ruby: "2.7" 46 | rails: ~> 5.1.0 47 | - ruby: "2.7" 48 | rails: ~> 5.2.0 49 | - ruby: "2.7" 50 | rails: ~> 6.0.0 51 | - ruby: "2.7" 52 | rails: ~> 6.1.0 53 | - ruby: "2.7" 54 | rails: ~> 7.0.0 55 | - ruby: "2.7" 56 | rails: ~> 7.1.0 57 | 58 | - ruby: "3.0" 59 | rails: ~> 6.0.0 60 | - ruby: "3.0" 61 | rails: ~> 6.1.0 62 | - ruby: "3.0" 63 | rails: ~> 7.0.0 64 | - ruby: "3.0" 65 | rails: ~> 7.1.0 66 | 67 | - ruby: "3.1" 68 | rails: ~> 6.0.0 69 | - ruby: "3.1" 70 | rails: ~> 6.1.0 71 | - ruby: "3.1" 72 | rails: ~> 7.0.0 73 | - ruby: "3.1" 74 | rails: ~> 7.1.0 75 | - ruby: "3.1" 76 | rails: edge 77 | 78 | - ruby: "3.2" 79 | rails: ~> 6.0.0 80 | - ruby: "3.2" 81 | rails: ~> 6.1.0 82 | - ruby: "3.2" 83 | rails: ~> 7.0.0 84 | - ruby: "3.2" 85 | rails: ~> 7.1.0 86 | - ruby: "3.2" 87 | rails: edge 88 | 89 | - ruby: "3.3" 90 | rails: ~> 6.0.0 91 | - ruby: "3.3" 92 | rails: ~> 6.1.0 93 | - ruby: "3.3" 94 | rails: ~> 7.0.0 95 | - ruby: "3.3" 96 | rails: ~> 7.1.0 97 | - ruby: "3.3" 98 | rails: edge 99 | 100 | - ruby: head 101 | rails: ~> 7.1.0 102 | - ruby: head 103 | rails: edge 104 | 105 | env: 106 | RAILS_VERSION: ${{ matrix.rails }} 107 | runs-on: ubuntu-latest 108 | steps: 109 | - uses: actions/checkout@v4 110 | - uses: ruby/setup-ruby@v1 111 | with: 112 | ruby-version: ${{ matrix.ruby }} 113 | bundler: ${{ matrix.bundler }} 114 | - name: Run bundle update 115 | run: bundle update 116 | - name: Run tests 117 | run: bin/rake 118 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.rubocop-* 3 | /.yardoc 4 | /Gemfile.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - https://raw.githubusercontent.com/cookpad/global-style-guides/master/.rubocop.yml 3 | 4 | AllCops: 5 | TargetRubyVersion: 2.5 6 | 7 | # Disable this as this does not apply to rack-test 8 | Rails/HttpPositionalArguments: 9 | Enabled: false 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, 8 | body size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual 10 | identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an 52 | appointed representative at an online or offline event. Representation of a 53 | project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kaihatsu@cookpad.com. All complaints 59 | will be reviewed and investigated and will result in a response that is deemed 60 | necessary and appropriate to the circumstances. The project team is obligated 61 | to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 71 | version 1.4, available at 72 | [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | [version]: http://contributor-covenant.org/version/1/4/ 76 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | if ENV["RAILS_VERSION"] == "edge" 4 | gem "rails", git: "https://github.com/rails/rails.git", branch: "main" 5 | end 6 | 7 | # Lock loofah to old version for Ruby 2.4 8 | unless RUBY_VERSION > "2.5" 9 | gem "loofah", "~> 2.20.0" 10 | end 11 | 12 | gemspec 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Cookpad Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OmniAuth - Rails CSRF Protection 2 | 3 | This gem provides a mitigation against [CVE-2015-9284] (Cross-Site Request 4 | Forgery on the request phase when using OmniAuth gem with a Ruby on Rails 5 | application) by implementing a CSRF token verifier that directly uses 6 | `ActionController::RequestForgeryProtection` code from Rails. 7 | 8 | [CVE-2015-9284]: https://nvd.nist.gov/vuln/detail/CVE-2015-9284 9 | 10 | ## Usage 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | gem "omniauth-rails_csrf_protection" 16 | ``` 17 | 18 | Then run `bundle install` to install this gem. 19 | 20 | You will then need to verify that all links in your application that would 21 | initiate OAuth request phase are being converted to a HTTP POST form that 22 | contains `authenticity_token` value. This might simply be done by changing all 23 | `link_to` to `button_to`, or use `link_to ..., method: :post`. 24 | 25 | ## Under the Hood 26 | 27 | This gem does a few things to your application: 28 | 29 | * Disable access to the OAuth request phase using HTTP GET method. 30 | * Insert a Rails CSRF token verifier at the before request phase. 31 | 32 | These actions mitigate you from the attack vector described in [CVE-2015-9284]. 33 | 34 | ## Contributing 35 | 36 | Bug reports and pull requests are welcome on GitHub. This project is 37 | intended to be a safe, welcoming space for collaboration, and contributors are 38 | expected to adhere to the 39 | [Contributor Covenant](http://contributor-covenant.org) code of conduct. 40 | 41 | ## License 42 | 43 | The gem is available as open source under the terms of the 44 | [MIT License](https://opensource.org/licenses/MIT). 45 | 46 | ## Code of Conduct 47 | 48 | Everyone interacting in the this project’s codebases, issue trackers, chat 49 | rooms and mailing lists is expected to follow the 50 | [code of conduct](https://github.com/cookpad/omniauth-rails_csrf_protection/blob/main/CODE_OF_CONDUCT.md). 51 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("rake", "rake") 28 | -------------------------------------------------------------------------------- /lib/omniauth/rails_csrf_protection.rb: -------------------------------------------------------------------------------- 1 | require "omniauth/rails_csrf_protection/version" 2 | require "omniauth/rails_csrf_protection/railtie" 3 | -------------------------------------------------------------------------------- /lib/omniauth/rails_csrf_protection/railtie.rb: -------------------------------------------------------------------------------- 1 | require "omniauth" 2 | require "omniauth/rails_csrf_protection/token_verifier" 3 | 4 | module OmniAuth 5 | module RailsCsrfProtection 6 | class Railtie < Rails::Railtie 7 | initializer "omniauth-rails_csrf_protection.initialize" do 8 | OmniAuth.config.request_validation_phase = TokenVerifier.new 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/omniauth/rails_csrf_protection/token_verifier.rb: -------------------------------------------------------------------------------- 1 | require "active_support/configurable" 2 | require "action_controller" 3 | 4 | module OmniAuth 5 | module RailsCsrfProtection 6 | # Provides a callable method that verifies Cross-Site Request Forgery 7 | # protection token. This class includes 8 | # `ActionController::RequestForgeryProtection` directly and utilizes 9 | # `verified_request?` method to match the way Rails performs token 10 | # verification in Rails controllers. 11 | # 12 | # If you like to learn more about how Rails generate and verify 13 | # authenticity token, you can find the source code at 14 | # https://github.com/rails/rails/blob/v5.2.2/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L217-L240. 15 | class TokenVerifier 16 | include ActiveSupport::Configurable 17 | include ActionController::RequestForgeryProtection 18 | 19 | # `ActionController::RequestForgeryProtection` contains a few 20 | # configurable options. As we want to make sure that our configuration is 21 | # the same as what being set in `ActionController::Base`, we should make 22 | # all out configuration methods to delegate to `ActionController::Base`. 23 | config.each_key do |configuration_name| 24 | undef_method configuration_name 25 | define_method configuration_name do 26 | ActionController::Base.config[configuration_name] 27 | end 28 | end 29 | 30 | def call(env) 31 | dup._call(env) 32 | end 33 | 34 | def _call(env) 35 | @request = ActionDispatch::Request.new(env.dup) 36 | 37 | unless verified_request? 38 | raise ActionController::InvalidAuthenticityToken 39 | end 40 | end 41 | 42 | private 43 | 44 | attr_reader :request 45 | delegate :params, :session, to: :request 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/omniauth/rails_csrf_protection/version.rb: -------------------------------------------------------------------------------- 1 | module OmniAuth 2 | module RailsCsrfProtection 3 | VERSION = "1.0.2".freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /omniauth-rails_csrf_protection.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("lib", __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require "omniauth/rails_csrf_protection/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "omniauth-rails_csrf_protection" 7 | spec.version = OmniAuth::RailsCsrfProtection::VERSION 8 | spec.authors = ["Cookpad Inc."] 9 | spec.email = ["kaihatsu@cookpad.com"] 10 | 11 | spec.summary = <<~SUMMARY 12 | Provides CSRF protection on OmniAuth request endpoint on Rails application. 13 | SUMMARY 14 | 15 | spec.description = <<~DESCRIPTION 16 | This gem provides a mitigation against CVE-2015-9284 (Cross-Site Request 17 | Forgery on the request phrase when using OmniAuth gem with a Ruby on Rails 18 | application) by implementing a CSRF token verifier that directly utilize 19 | `ActionController::RequestForgeryProtection` code from Rails. 20 | DESCRIPTION 21 | 22 | spec.homepage = "https://github.com/cookpad/omniauth-rails_csrf_protection" 23 | spec.license = "MIT" 24 | 25 | spec.files = Dir["lib/**/*.rb", "LICENSE.txt", "README.md"] 26 | spec.test_files = Dir["test/**/*.rb"] 27 | 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_dependency "actionpack", ">= 4.2" 31 | spec.add_dependency "omniauth", "~> 2.0" 32 | 33 | spec.add_development_dependency "bundler" 34 | spec.add_development_dependency "minitest" 35 | 36 | # We set requirement for Edge Rails in the Gemfile 37 | unless ENV["RAILS_VERSION"] == "edge" 38 | spec.add_development_dependency "rails", ENV["RAILS_VERSION"] 39 | end 40 | 41 | spec.add_development_dependency "rake" 42 | end 43 | -------------------------------------------------------------------------------- /test/application_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationTest < Minitest::Test 4 | include Rack::Test::Methods 5 | 6 | def test_request_phrase_not_accessible_via_get 7 | get "/auth/developer" 8 | 9 | assert last_response.not_found? 10 | end 11 | 12 | def test_request_phrase_without_token_via_post 13 | post "/auth/developer" 14 | follow_redirect! 15 | 16 | assert last_response.not_found? 17 | end 18 | 19 | def test_request_phrase_with_bad_token_via_post 20 | post "/auth/developer", authenticity_token: "BAD_TOKEN" 21 | follow_redirect! 22 | 23 | assert last_response.not_found? 24 | end 25 | 26 | def test_request_phrase_with_correct_token_via_post 27 | post "/auth/developer", authenticity_token: authenticity_token 28 | 29 | assert last_response.ok? 30 | end 31 | 32 | private 33 | 34 | def app 35 | Rails.application 36 | end 37 | 38 | def authenticity_token 39 | get "/token" 40 | last_response.body 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 2 | 3 | # Simple Rails application template, based on Rails issue template 4 | # https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb 5 | 6 | # Helper method to silence warnings from bundler/inline 7 | def silence_warnings 8 | old_verbose, $VERBOSE = $VERBOSE, nil 9 | yield 10 | ensure 11 | $VERBOSE = old_verbose 12 | end 13 | 14 | silence_warnings do 15 | require "bundler/inline" 16 | 17 | # Define dependencies required by this test app 18 | gemfile do 19 | source "https://rubygems.org" 20 | 21 | if ENV["RAILS_VERSION"] == "edge" 22 | gem "rails", git: "https://github.com/rails/rails.git", branch: "main" 23 | else 24 | gem "rails" 25 | end 26 | 27 | gem "omniauth" 28 | gem "omniauth-rails_csrf_protection", path: File.expand_path("..", __dir__) 29 | end 30 | end 31 | 32 | puts "Running test against Rails #{Rails.version}" 33 | 34 | require "rack/test" 35 | require "action_controller/railtie" 36 | require "minitest/autorun" 37 | 38 | # Build a test application which uses OmniAuth 39 | class TestApp < Rails::Application 40 | config.root = __dir__ 41 | config.session_store :cookie_store, key: "cookie_store_key" 42 | config.secret_key_base = "secret_key_base" 43 | config.eager_load = false 44 | config.hosts = [] 45 | 46 | # This allow us to send all logs to STDOUT if we run test wth `VERBOSE=1` 47 | config.logger = if ENV["VERBOSE"] 48 | Logger.new($stdout) 49 | else 50 | Logger.new("/dev/null") 51 | end 52 | Rails.logger = config.logger 53 | OmniAuth.config.logger = Rails.logger 54 | 55 | # Setup a simple OmniAuth configuration with only developer provider 56 | config.middleware.use OmniAuth::Builder do 57 | provider :developer 58 | end 59 | 60 | # We need to call initialize! to run all railties 61 | initialize! 62 | 63 | # Define our custom routes. This needs to be called after initialize! 64 | routes.draw do 65 | get "token" => "application#token" 66 | end 67 | end 68 | 69 | # A small test controller which we use to retrive the valid authenticity token 70 | class ApplicationController < ActionController::Base 71 | def token 72 | render plain: form_authenticity_token 73 | end 74 | end 75 | --------------------------------------------------------------------------------