rack-protection 2.0.5 → 3.0.5
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 +4 -4
 - data/Gemfile +7 -3
 - data/Rakefile +24 -22
 - data/lib/rack/protection/authenticity_token.rb +85 -26
 - data/lib/rack/protection/base.rb +23 -15
 - data/lib/rack/protection/content_security_policy.rb +9 -9
 - data/lib/rack/protection/cookie_tossing.rb +7 -5
 - data/lib/rack/protection/encrypted_cookie.rb +273 -0
 - data/lib/rack/protection/encryptor.rb +62 -0
 - data/lib/rack/protection/escaped_params.rb +14 -10
 - data/lib/rack/protection/form_token.rb +3 -1
 - data/lib/rack/protection/frame_options.rb +3 -1
 - data/lib/rack/protection/http_origin.rb +11 -8
 - data/lib/rack/protection/ip_spoofing.rb +7 -3
 - data/lib/rack/protection/json_csrf.rb +6 -3
 - data/lib/rack/protection/path_traversal.rb +12 -17
 - data/lib/rack/protection/referrer_policy.rb +27 -0
 - data/lib/rack/protection/remote_referrer.rb +2 -0
 - data/lib/rack/protection/remote_token.rb +2 -0
 - data/lib/rack/protection/session_hijacking.rb +8 -7
 - data/lib/rack/protection/strict_transport.rb +4 -2
 - data/lib/rack/protection/version.rb +3 -1
 - data/lib/rack/protection/xss_header.rb +3 -1
 - data/lib/rack/protection.rb +9 -2
 - data/lib/rack-protection.rb +1 -1
 - data/lib/rack_protection.rb +3 -0
 - data/rack-protection.gemspec +29 -24
 - metadata +18 -14
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 7fef35d427c0ff406165fdf913c53089e65f94d759aaf5610aebd5ef0c43cd30
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 4a458e4fada2015274bde82e4209bfe4c94618227fc7a2dc8df198258c1b7404
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: e8b1ba3b66ae172be989c43133f111fbe416706df83735f51aa785146242bbab8e55c5b0fa4667dff165ac462db518c1209f9957ec3bf95744da1dc4881cd5c9
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: cd46380780ad4c7078a6fc31f46cfaa6dda79bcbf2b47cf6e973592d85c35d4ef1c0bff5fee0b910e29e5cb579307447eae4d4c5880f2440586ee204a9d37da8
         
     | 
    
        data/Gemfile
    CHANGED
    
    | 
         @@ -1,13 +1,17 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            source 'https://rubygems.org'
         
     | 
| 
       2 
4 
     | 
    
         
             
            # encoding: utf-8
         
     | 
| 
       3 
5 
     | 
    
         | 
| 
       4 
6 
     | 
    
         
             
            gem 'rake'
         
     | 
| 
       5 
7 
     | 
    
         | 
| 
       6 
8 
     | 
    
         
             
            rack_version = ENV['rack'].to_s
         
     | 
| 
       7 
     | 
    
         
            -
            rack_version = nil if rack_version.empty?  
     | 
| 
       8 
     | 
    
         
            -
            rack_version = {: 
     | 
| 
      
 9 
     | 
    
         
            +
            rack_version = nil if rack_version.empty? || (rack_version == 'stable')
         
     | 
| 
      
 10 
     | 
    
         
            +
            rack_version = { github: 'rack/rack' } if rack_version == 'main'
         
     | 
| 
       9 
11 
     | 
    
         
             
            gem 'rack', rack_version
         
     | 
| 
       10 
12 
     | 
    
         | 
| 
       11 
13 
     | 
    
         
             
            gem 'sinatra', path: '..'
         
     | 
| 
       12 
14 
     | 
    
         | 
| 
       13 
15 
     | 
    
         
             
            gemspec
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            gem 'rack-test', github: 'rack/rack-test'
         
     | 
    
        data/Rakefile
    CHANGED
    
    | 
         @@ -1,53 +1,55 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            #  
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            $LOAD_PATH.unshift File.expand_path('lib', __dir__)
         
     | 
| 
       3 
4 
     | 
    
         | 
| 
       4 
5 
     | 
    
         
             
            begin
         
     | 
| 
       5 
6 
     | 
    
         
             
              require 'bundler'
         
     | 
| 
       6 
7 
     | 
    
         
             
              Bundler::GemHelper.install_tasks
         
     | 
| 
       7 
8 
     | 
    
         
             
            rescue LoadError => e
         
     | 
| 
       8 
     | 
    
         
            -
               
     | 
| 
      
 9 
     | 
    
         
            +
              warn e
         
     | 
| 
       9 
10 
     | 
    
         
             
            end
         
     | 
| 
       10 
11 
     | 
    
         | 
| 
       11 
     | 
    
         
            -
            desc  
     | 
| 
       12 
     | 
    
         
            -
            task(:spec) { ruby '-S rspec 
     | 
| 
      
 12 
     | 
    
         
            +
            desc 'run specs'
         
     | 
| 
      
 13 
     | 
    
         
            +
            task(:spec) { ruby '-S rspec' }
         
     | 
| 
       13 
14 
     | 
    
         | 
| 
       14 
15 
     | 
    
         
             
            namespace :doc do
         
     | 
| 
       15 
16 
     | 
    
         
             
              task :readmes do
         
     | 
| 
       16 
17 
     | 
    
         
             
                Dir.glob 'lib/rack/protection/*.rb' do |file|
         
     | 
| 
       17 
18 
     | 
    
         
             
                  excluded_files = %w[lib/rack/protection/base.rb lib/rack/protection/version.rb]
         
     | 
| 
       18 
19 
     | 
    
         
             
                  next if excluded_files.include?(file)
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
       19 
21 
     | 
    
         
             
                  doc  = File.read(file)[/^  module Protection(\n)+(    #[^\n]*\n)*/m].scan(/^ *#(?!#) ?(.*)\n/).join("\n")
         
     | 
| 
       20 
     | 
    
         
            -
                  file = "doc/#{file[4..-4].tr( 
     | 
| 
       21 
     | 
    
         
            -
                  Dir.mkdir  
     | 
| 
      
 22 
     | 
    
         
            +
                  file = "doc/#{file[4..-4].tr('/_', '-')}.rdoc"
         
     | 
| 
      
 23 
     | 
    
         
            +
                  Dir.mkdir 'doc' unless File.directory? 'doc'
         
     | 
| 
       22 
24 
     | 
    
         
             
                  puts "writing #{file}"
         
     | 
| 
       23 
     | 
    
         
            -
                  File.open(file,  
     | 
| 
      
 25 
     | 
    
         
            +
                  File.open(file, 'w') { |f| f << doc }
         
     | 
| 
       24 
26 
     | 
    
         
             
                end
         
     | 
| 
       25 
27 
     | 
    
         
             
              end
         
     | 
| 
       26 
28 
     | 
    
         | 
| 
       27 
29 
     | 
    
         
             
              task :index do
         
     | 
| 
       28 
     | 
    
         
            -
                doc = File.read( 
     | 
| 
       29 
     | 
    
         
            -
                file =  
     | 
| 
       30 
     | 
    
         
            -
                Dir.mkdir  
     | 
| 
      
 30 
     | 
    
         
            +
                doc = File.read('README.md')
         
     | 
| 
      
 31 
     | 
    
         
            +
                file = 'doc/rack-protection-readme.md'
         
     | 
| 
      
 32 
     | 
    
         
            +
                Dir.mkdir 'doc' unless File.directory? 'doc'
         
     | 
| 
       31 
33 
     | 
    
         
             
                puts "writing #{file}"
         
     | 
| 
       32 
     | 
    
         
            -
                File.open(file,  
     | 
| 
      
 34 
     | 
    
         
            +
                File.open(file, 'w') { |f| f << doc }
         
     | 
| 
       33 
35 
     | 
    
         
             
              end
         
     | 
| 
       34 
36 
     | 
    
         | 
| 
       35 
     | 
    
         
            -
              task : 
     | 
| 
      
 37 
     | 
    
         
            +
              task all: %i[readmes index]
         
     | 
| 
       36 
38 
     | 
    
         
             
            end
         
     | 
| 
       37 
39 
     | 
    
         | 
| 
       38 
     | 
    
         
            -
            desc  
     | 
| 
       39 
     | 
    
         
            -
            task : 
     | 
| 
      
 40 
     | 
    
         
            +
            desc 'generate documentation'
         
     | 
| 
      
 41 
     | 
    
         
            +
            task doc: 'doc:all'
         
     | 
| 
       40 
42 
     | 
    
         | 
| 
       41 
     | 
    
         
            -
            desc  
     | 
| 
      
 43 
     | 
    
         
            +
            desc 'generate gemspec'
         
     | 
| 
       42 
44 
     | 
    
         
             
            task 'rack-protection.gemspec' do
         
     | 
| 
       43 
45 
     | 
    
         
             
              require 'rack/protection/version'
         
     | 
| 
       44 
46 
     | 
    
         
             
              content = File.binread 'rack-protection.gemspec'
         
     | 
| 
       45 
47 
     | 
    
         | 
| 
       46 
48 
     | 
    
         
             
              # fetch data
         
     | 
| 
       47 
49 
     | 
    
         
             
              fields = {
         
     | 
| 
       48 
     | 
    
         
            -
                : 
     | 
| 
       49 
     | 
    
         
            -
                : 
     | 
| 
       50 
     | 
    
         
            -
                : 
     | 
| 
      
 50 
     | 
    
         
            +
                authors: `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
         
     | 
| 
      
 51 
     | 
    
         
            +
                email: ['mail@zzak.io', 'konstantin.haase@gmail.com'],
         
     | 
| 
      
 52 
     | 
    
         
            +
                files: %w[License README.md Rakefile Gemfile rack-protection.gemspec] + Dir['lib/**/*']
         
     | 
| 
       51 
53 
     | 
    
         
             
              }
         
     | 
| 
       52 
54 
     | 
    
         | 
| 
       53 
55 
     | 
    
         
             
              # insert data
         
     | 
| 
         @@ -67,6 +69,6 @@ task 'rack-protection.gemspec' do 
     | 
|
| 
       67 
69 
     | 
    
         
             
              File.open('rack-protection.gemspec', 'w') { |f| f << content }
         
     | 
| 
       68 
70 
     | 
    
         
             
            end
         
     | 
| 
       69 
71 
     | 
    
         | 
| 
       70 
     | 
    
         
            -
            task : 
     | 
| 
       71 
     | 
    
         
            -
            task : 
     | 
| 
       72 
     | 
    
         
            -
            task : 
     | 
| 
      
 72 
     | 
    
         
            +
            task gemspec: 'rack-protection.gemspec'
         
     | 
| 
      
 73 
     | 
    
         
            +
            task default: :spec
         
     | 
| 
      
 74 
     | 
    
         
            +
            task test: :spec
         
     | 
| 
         @@ -1,5 +1,8 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
       1 
3 
     | 
    
         
             
            require 'rack/protection'
         
     | 
| 
       2 
4 
     | 
    
         
             
            require 'securerandom'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'openssl'
         
     | 
| 
       3 
6 
     | 
    
         
             
            require 'base64'
         
     | 
| 
       4 
7 
     | 
    
         | 
| 
       5 
8 
     | 
    
         
             
            module Rack
         
     | 
| 
         @@ -16,7 +19,10 @@ module Rack 
     | 
|
| 
       16 
19 
     | 
    
         
             
                # It checks the <tt>X-CSRF-Token</tt> header and the <tt>POST</tt> form
         
     | 
| 
       17 
20 
     | 
    
         
             
                # data.
         
     | 
| 
       18 
21 
     | 
    
         
             
                #
         
     | 
| 
       19 
     | 
    
         
            -
                #  
     | 
| 
      
 22 
     | 
    
         
            +
                # It is not OOTB-compatible with the {rack-csrf}[https://rubygems.org/gems/rack_csrf] gem.
         
     | 
| 
      
 23 
     | 
    
         
            +
                # For that, the following patch needs to be applied:
         
     | 
| 
      
 24 
     | 
    
         
            +
                #
         
     | 
| 
      
 25 
     | 
    
         
            +
                #   Rack::Protection::AuthenticityToken.default_options(key: "csrf.token", authenticity_param: "_csrf")
         
     | 
| 
       20 
26 
     | 
    
         
             
                #
         
     | 
| 
       21 
27 
     | 
    
         
             
                # == Options
         
     | 
| 
       22 
28 
     | 
    
         
             
                #
         
     | 
| 
         @@ -24,6 +30,13 @@ module Rack 
     | 
|
| 
       24 
30 
     | 
    
         
             
                #                                the token on a request. Default value:
         
     | 
| 
       25 
31 
     | 
    
         
             
                #                                <tt>"authenticity_token"</tt>
         
     | 
| 
       26 
32 
     | 
    
         
             
                #
         
     | 
| 
      
 33 
     | 
    
         
            +
                # [<tt>:key</tt>] the name of the param that should contain
         
     | 
| 
      
 34 
     | 
    
         
            +
                #                                the token in the session. Default value:
         
     | 
| 
      
 35 
     | 
    
         
            +
                #                                <tt>:csrf</tt>
         
     | 
| 
      
 36 
     | 
    
         
            +
                #
         
     | 
| 
      
 37 
     | 
    
         
            +
                # [<tt>:allow_if</tt>] a proc for custom allow/deny logic. Default value:
         
     | 
| 
      
 38 
     | 
    
         
            +
                #                                <tt>nil</tt>
         
     | 
| 
      
 39 
     | 
    
         
            +
                #
         
     | 
| 
       27 
40 
     | 
    
         
             
                # == Example: Forms application
         
     | 
| 
       28 
41 
     | 
    
         
             
                #
         
     | 
| 
       29 
42 
     | 
    
         
             
                # To show what the AuthenticityToken does, this section includes a sample
         
     | 
| 
         @@ -63,7 +76,7 @@ module Rack 
     | 
|
| 
       63 
76 
     | 
    
         
             
                #             <h1>With Authenticity Token</h1>
         
     | 
| 
       64 
77 
     | 
    
         
             
                #             <p>This successfully takes you to back to this form.</p>
         
     | 
| 
       65 
78 
     | 
    
         
             
                #             <form action="" method="post">
         
     | 
| 
       66 
     | 
    
         
            -
                #               <input type="hidden" name="authenticity_token" value="#{env['rack.session'] 
     | 
| 
      
 79 
     | 
    
         
            +
                #               <input type="hidden" name="authenticity_token" value="#{Rack::Protection::AuthenticityToken.token(env['rack.session'])}" />
         
     | 
| 
       67 
80 
     | 
    
         
             
                #               <input type="text" name="foo" />
         
     | 
| 
       68 
81 
     | 
    
         
             
                #               <input type="submit" />
         
     | 
| 
       69 
82 
     | 
    
         
             
                #             </form>
         
     | 
| 
         @@ -84,42 +97,57 @@ module Rack 
     | 
|
| 
       84 
97 
     | 
    
         
             
                class AuthenticityToken < Base
         
     | 
| 
       85 
98 
     | 
    
         
             
                  TOKEN_LENGTH = 32
         
     | 
| 
       86 
99 
     | 
    
         | 
| 
       87 
     | 
    
         
            -
                  default_options : 
     | 
| 
       88 
     | 
    
         
            -
                                  : 
     | 
| 
      
 100 
     | 
    
         
            +
                  default_options authenticity_param: 'authenticity_token',
         
     | 
| 
      
 101 
     | 
    
         
            +
                                  key: :csrf,
         
     | 
| 
      
 102 
     | 
    
         
            +
                                  allow_if: nil
         
     | 
| 
       89 
103 
     | 
    
         | 
| 
       90 
     | 
    
         
            -
                  def self.token(session)
         
     | 
| 
       91 
     | 
    
         
            -
                     
     | 
| 
      
 104 
     | 
    
         
            +
                  def self.token(session, path: nil, method: :post)
         
     | 
| 
      
 105 
     | 
    
         
            +
                    new(nil).mask_authenticity_token(session, path: path, method: method)
         
     | 
| 
       92 
106 
     | 
    
         
             
                  end
         
     | 
| 
       93 
107 
     | 
    
         | 
| 
       94 
108 
     | 
    
         
             
                  def self.random_token
         
     | 
| 
       95 
     | 
    
         
            -
                    SecureRandom. 
     | 
| 
      
 109 
     | 
    
         
            +
                    SecureRandom.urlsafe_base64(TOKEN_LENGTH, padding: false)
         
     | 
| 
       96 
110 
     | 
    
         
             
                  end
         
     | 
| 
       97 
111 
     | 
    
         | 
| 
       98 
112 
     | 
    
         
             
                  def accepts?(env)
         
     | 
| 
       99 
     | 
    
         
            -
                    session = session 
     | 
| 
      
 113 
     | 
    
         
            +
                    session = session(env)
         
     | 
| 
       100 
114 
     | 
    
         
             
                    set_token(session)
         
     | 
| 
       101 
115 
     | 
    
         | 
| 
       102 
116 
     | 
    
         
             
                    safe?(env) ||
         
     | 
| 
       103 
     | 
    
         
            -
                      valid_token?( 
     | 
| 
       104 
     | 
    
         
            -
                      valid_token?( 
     | 
| 
       105 
     | 
    
         
            -
                       
     | 
| 
      
 117 
     | 
    
         
            +
                      valid_token?(env, env['HTTP_X_CSRF_TOKEN']) ||
         
     | 
| 
      
 118 
     | 
    
         
            +
                      valid_token?(env, Request.new(env).params[options[:authenticity_param]]) ||
         
     | 
| 
      
 119 
     | 
    
         
            +
                      options[:allow_if]&.call(env)
         
     | 
| 
      
 120 
     | 
    
         
            +
                  rescue StandardError
         
     | 
| 
      
 121 
     | 
    
         
            +
                    false
         
     | 
| 
       106 
122 
     | 
    
         
             
                  end
         
     | 
| 
       107 
123 
     | 
    
         | 
| 
       108 
     | 
    
         
            -
                  def mask_authenticity_token(session)
         
     | 
| 
       109 
     | 
    
         
            -
                     
     | 
| 
      
 124 
     | 
    
         
            +
                  def mask_authenticity_token(session, path: nil, method: :post)
         
     | 
| 
      
 125 
     | 
    
         
            +
                    set_token(session)
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                    token = if path && method
         
     | 
| 
      
 128 
     | 
    
         
            +
                              per_form_token(session, path, method)
         
     | 
| 
      
 129 
     | 
    
         
            +
                            else
         
     | 
| 
      
 130 
     | 
    
         
            +
                              global_token(session)
         
     | 
| 
      
 131 
     | 
    
         
            +
                            end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
       110 
133 
     | 
    
         
             
                    mask_token(token)
         
     | 
| 
       111 
134 
     | 
    
         
             
                  end
         
     | 
| 
       112 
135 
     | 
    
         | 
| 
      
 136 
     | 
    
         
            +
                  GLOBAL_TOKEN_IDENTIFIER = '!real_csrf_token'
         
     | 
| 
      
 137 
     | 
    
         
            +
                  private_constant :GLOBAL_TOKEN_IDENTIFIER
         
     | 
| 
      
 138 
     | 
    
         
            +
             
     | 
| 
       113 
139 
     | 
    
         
             
                  private
         
     | 
| 
       114 
140 
     | 
    
         | 
| 
       115 
141 
     | 
    
         
             
                  def set_token(session)
         
     | 
| 
       116 
     | 
    
         
            -
                    session[: 
     | 
| 
      
 142 
     | 
    
         
            +
                    session[options[:key]] ||= self.class.random_token
         
     | 
| 
       117 
143 
     | 
    
         
             
                  end
         
     | 
| 
       118 
144 
     | 
    
         | 
| 
       119 
145 
     | 
    
         
             
                  # Checks the client's masked token to see if it matches the
         
     | 
| 
       120 
146 
     | 
    
         
             
                  # session token.
         
     | 
| 
       121 
     | 
    
         
            -
                  def valid_token?( 
     | 
| 
       122 
     | 
    
         
            -
                    return false if token.nil? || token.empty?
         
     | 
| 
      
 147 
     | 
    
         
            +
                  def valid_token?(env, token)
         
     | 
| 
      
 148 
     | 
    
         
            +
                    return false if token.nil? || !token.is_a?(String) || token.empty?
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                    session = session(env)
         
     | 
| 
       123 
151 
     | 
    
         | 
| 
       124 
152 
     | 
    
         
             
                    begin
         
     | 
| 
       125 
153 
     | 
    
         
             
                      token = decode_token(token)
         
     | 
| 
         @@ -131,13 +159,13 @@ module Rack 
     | 
|
| 
       131 
159 
     | 
    
         
             
                    # to handle any unmasked tokens that we've issued without error.
         
     | 
| 
       132 
160 
     | 
    
         | 
| 
       133 
161 
     | 
    
         
             
                    if unmasked_token?(token)
         
     | 
| 
       134 
     | 
    
         
            -
                      compare_with_real_token 
     | 
| 
       135 
     | 
    
         
            -
             
     | 
| 
      
 162 
     | 
    
         
            +
                      compare_with_real_token(token, session)
         
     | 
| 
       136 
163 
     | 
    
         
             
                    elsif masked_token?(token)
         
     | 
| 
       137 
164 
     | 
    
         
             
                      token = unmask_token(token)
         
     | 
| 
       138 
165 
     | 
    
         | 
| 
       139 
     | 
    
         
            -
                       
     | 
| 
       140 
     | 
    
         
            -
             
     | 
| 
      
 166 
     | 
    
         
            +
                      compare_with_global_token(token, session) ||
         
     | 
| 
      
 167 
     | 
    
         
            +
                        compare_with_real_token(token, session) ||
         
     | 
| 
      
 168 
     | 
    
         
            +
                        compare_with_per_form_token(token, session, Request.new(env))
         
     | 
| 
       141 
169 
     | 
    
         
             
                    else
         
     | 
| 
       142 
170 
     | 
    
         
             
                      false # Token is malformed
         
     | 
| 
       143 
171 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -147,7 +175,6 @@ module Rack 
     | 
|
| 
       147 
175 
     | 
    
         
             
                  # on each request. The masking is used to mitigate SSL attacks
         
     | 
| 
       148 
176 
     | 
    
         
             
                  # like BREACH.
         
     | 
| 
       149 
177 
     | 
    
         
             
                  def mask_token(token)
         
     | 
| 
       150 
     | 
    
         
            -
                    token = decode_token(token)
         
     | 
| 
       151 
178 
     | 
    
         
             
                    one_time_pad = SecureRandom.random_bytes(token.length)
         
     | 
| 
       152 
179 
     | 
    
         
             
                    encrypted_token = xor_byte_strings(one_time_pad, token)
         
     | 
| 
       153 
180 
     | 
    
         
             
                    masked_token = one_time_pad + encrypted_token
         
     | 
| 
         @@ -160,7 +187,7 @@ module Rack 
     | 
|
| 
       160 
187 
     | 
    
         
             
                    # value and decrypt it
         
     | 
| 
       161 
188 
     | 
    
         
             
                    token_length = masked_token.length / 2
         
     | 
| 
       162 
189 
     | 
    
         
             
                    one_time_pad = masked_token[0...token_length]
         
     | 
| 
       163 
     | 
    
         
            -
                    encrypted_token = masked_token[token_length 
     | 
| 
      
 190 
     | 
    
         
            +
                    encrypted_token = masked_token[token_length..]
         
     | 
| 
       164 
191 
     | 
    
         
             
                    xor_byte_strings(one_time_pad, encrypted_token)
         
     | 
| 
       165 
192 
     | 
    
         
             
                  end
         
     | 
| 
       166 
193 
     | 
    
         | 
| 
         @@ -176,20 +203,52 @@ module Rack 
     | 
|
| 
       176 
203 
     | 
    
         
             
                    secure_compare(token, real_token(session))
         
     | 
| 
       177 
204 
     | 
    
         
             
                  end
         
     | 
| 
       178 
205 
     | 
    
         | 
| 
      
 206 
     | 
    
         
            +
                  def compare_with_global_token(token, session)
         
     | 
| 
      
 207 
     | 
    
         
            +
                    secure_compare(token, global_token(session))
         
     | 
| 
      
 208 
     | 
    
         
            +
                  end
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
      
 210 
     | 
    
         
            +
                  def compare_with_per_form_token(token, session, request)
         
     | 
| 
      
 211 
     | 
    
         
            +
                    secure_compare(token,
         
     | 
| 
      
 212 
     | 
    
         
            +
                                   per_form_token(session, request.path.chomp('/'), request.request_method))
         
     | 
| 
      
 213 
     | 
    
         
            +
                  end
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
       179 
215 
     | 
    
         
             
                  def real_token(session)
         
     | 
| 
       180 
     | 
    
         
            -
                    decode_token(session[: 
     | 
| 
      
 216 
     | 
    
         
            +
                    decode_token(session[options[:key]])
         
     | 
| 
      
 217 
     | 
    
         
            +
                  end
         
     | 
| 
      
 218 
     | 
    
         
            +
             
     | 
| 
      
 219 
     | 
    
         
            +
                  def global_token(session)
         
     | 
| 
      
 220 
     | 
    
         
            +
                    token_hmac(session, GLOBAL_TOKEN_IDENTIFIER)
         
     | 
| 
      
 221 
     | 
    
         
            +
                  end
         
     | 
| 
      
 222 
     | 
    
         
            +
             
     | 
| 
      
 223 
     | 
    
         
            +
                  def per_form_token(session, path, method)
         
     | 
| 
      
 224 
     | 
    
         
            +
                    token_hmac(session, "#{path}##{method.downcase}")
         
     | 
| 
       181 
225 
     | 
    
         
             
                  end
         
     | 
| 
       182 
226 
     | 
    
         | 
| 
       183 
227 
     | 
    
         
             
                  def encode_token(token)
         
     | 
| 
       184 
     | 
    
         
            -
                    Base64. 
     | 
| 
      
 228 
     | 
    
         
            +
                    Base64.urlsafe_encode64(token)
         
     | 
| 
       185 
229 
     | 
    
         
             
                  end
         
     | 
| 
       186 
230 
     | 
    
         | 
| 
       187 
231 
     | 
    
         
             
                  def decode_token(token)
         
     | 
| 
       188 
     | 
    
         
            -
                    Base64. 
     | 
| 
      
 232 
     | 
    
         
            +
                    Base64.urlsafe_decode64(token)
         
     | 
| 
      
 233 
     | 
    
         
            +
                  end
         
     | 
| 
      
 234 
     | 
    
         
            +
             
     | 
| 
      
 235 
     | 
    
         
            +
                  def token_hmac(session, identifier)
         
     | 
| 
      
 236 
     | 
    
         
            +
                    OpenSSL::HMAC.digest(
         
     | 
| 
      
 237 
     | 
    
         
            +
                      OpenSSL::Digest.new('SHA256'),
         
     | 
| 
      
 238 
     | 
    
         
            +
                      real_token(session),
         
     | 
| 
      
 239 
     | 
    
         
            +
                      identifier
         
     | 
| 
      
 240 
     | 
    
         
            +
                    )
         
     | 
| 
       189 
241 
     | 
    
         
             
                  end
         
     | 
| 
       190 
242 
     | 
    
         | 
| 
       191 
243 
     | 
    
         
             
                  def xor_byte_strings(s1, s2)
         
     | 
| 
       192 
     | 
    
         
            -
                     
     | 
| 
      
 244 
     | 
    
         
            +
                    s2 = s2.dup
         
     | 
| 
      
 245 
     | 
    
         
            +
                    size = s1.bytesize
         
     | 
| 
      
 246 
     | 
    
         
            +
                    i = 0
         
     | 
| 
      
 247 
     | 
    
         
            +
                    while i < size
         
     | 
| 
      
 248 
     | 
    
         
            +
                      s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
         
     | 
| 
      
 249 
     | 
    
         
            +
                      i += 1
         
     | 
| 
      
 250 
     | 
    
         
            +
                    end
         
     | 
| 
      
 251 
     | 
    
         
            +
                    s2
         
     | 
| 
       193 
252 
     | 
    
         
             
                  end
         
     | 
| 
       194 
253 
     | 
    
         
             
                end
         
     | 
| 
       195 
254 
     | 
    
         
             
              end
         
     | 
    
        data/lib/rack/protection/base.rb
    CHANGED
    
    | 
         @@ -1,3 +1,5 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
       1 
3 
     | 
    
         
             
            require 'rack/protection'
         
     | 
| 
       2 
4 
     | 
    
         
             
            require 'rack/utils'
         
     | 
| 
       3 
5 
     | 
    
         
             
            require 'digest'
         
     | 
| 
         @@ -8,12 +10,12 @@ module Rack 
     | 
|
| 
       8 
10 
     | 
    
         
             
              module Protection
         
     | 
| 
       9 
11 
     | 
    
         
             
                class Base
         
     | 
| 
       10 
12 
     | 
    
         
             
                  DEFAULT_OPTIONS = {
         
     | 
| 
       11 
     | 
    
         
            -
                    : 
     | 
| 
       12 
     | 
    
         
            -
                    : 
     | 
| 
       13 
     | 
    
         
            -
                    : 
     | 
| 
       14 
     | 
    
         
            -
                    : 
     | 
| 
       15 
     | 
    
         
            -
                    : 
     | 
| 
       16 
     | 
    
         
            -
                    : 
     | 
| 
      
 13 
     | 
    
         
            +
                    reaction: :default_reaction, logging: true,
         
     | 
| 
      
 14 
     | 
    
         
            +
                    message: 'Forbidden', encryptor: Digest::SHA1,
         
     | 
| 
      
 15 
     | 
    
         
            +
                    session_key: 'rack.session', status: 403,
         
     | 
| 
      
 16 
     | 
    
         
            +
                    allow_empty_referrer: true,
         
     | 
| 
      
 17 
     | 
    
         
            +
                    report_key: 'protection.failed',
         
     | 
| 
      
 18 
     | 
    
         
            +
                    html_types: %w[text/html application/xhtml text/xml application/xml]
         
     | 
| 
       17 
19 
     | 
    
         
             
                  }
         
     | 
| 
       18 
20 
     | 
    
         | 
| 
       19 
21 
     | 
    
         
             
                  attr_reader :app, :options
         
     | 
| 
         @@ -31,7 +33,8 @@ module Rack 
     | 
|
| 
       31 
33 
     | 
    
         
             
                  end
         
     | 
| 
       32 
34 
     | 
    
         | 
| 
       33 
35 
     | 
    
         
             
                  def initialize(app, options = {})
         
     | 
| 
       34 
     | 
    
         
            -
                    @app 
     | 
| 
      
 36 
     | 
    
         
            +
                    @app = app
         
     | 
| 
      
 37 
     | 
    
         
            +
                    @options = default_options.merge(options)
         
     | 
| 
       35 
38 
     | 
    
         
             
                  end
         
     | 
| 
       36 
39 
     | 
    
         | 
| 
       37 
40 
     | 
    
         
             
                  def safe?(env)
         
     | 
| 
         @@ -52,24 +55,26 @@ module Rack 
     | 
|
| 
       52 
55 
     | 
    
         | 
| 
       53 
56 
     | 
    
         
             
                  def react(env)
         
     | 
| 
       54 
57 
     | 
    
         
             
                    result = send(options[:reaction], env)
         
     | 
| 
       55 
     | 
    
         
            -
                    result if Array === result  
     | 
| 
      
 58 
     | 
    
         
            +
                    result if (Array === result) && (result.size == 3)
         
     | 
| 
       56 
59 
     | 
    
         
             
                  end
         
     | 
| 
       57 
60 
     | 
    
         | 
| 
       58 
61 
     | 
    
         
             
                  def warn(env, message)
         
     | 
| 
       59 
62 
     | 
    
         
             
                    return unless options[:logging]
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
       60 
64 
     | 
    
         
             
                    l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
         
     | 
| 
       61 
65 
     | 
    
         
             
                    l.warn(message)
         
     | 
| 
       62 
66 
     | 
    
         
             
                  end
         
     | 
| 
       63 
67 
     | 
    
         | 
| 
       64 
68 
     | 
    
         
             
                  def instrument(env)
         
     | 
| 
       65 
     | 
    
         
            -
                    return unless i = options[:instrumenter]
         
     | 
| 
      
 69 
     | 
    
         
            +
                    return unless (i = options[:instrumenter])
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
       66 
71 
     | 
    
         
             
                    env['rack.protection.attack'] = self.class.name.split('::').last.downcase
         
     | 
| 
       67 
72 
     | 
    
         
             
                    i.instrument('rack.protection', env)
         
     | 
| 
       68 
73 
     | 
    
         
             
                  end
         
     | 
| 
       69 
74 
     | 
    
         | 
| 
       70 
75 
     | 
    
         
             
                  def deny(env)
         
     | 
| 
       71 
76 
     | 
    
         
             
                    warn env, "attack prevented by #{self.class}"
         
     | 
| 
       72 
     | 
    
         
            -
                    [options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
         
     | 
| 
      
 77 
     | 
    
         
            +
                    [options[:status], { 'Content-Type' => 'text/plain' }, [options[:message]]]
         
     | 
| 
       73 
78 
     | 
    
         
             
                  end
         
     | 
| 
       74 
79 
     | 
    
         | 
| 
       75 
80 
     | 
    
         
             
                  def report(env)
         
     | 
| 
         @@ -83,7 +88,8 @@ module Rack 
     | 
|
| 
       83 
88 
     | 
    
         | 
| 
       84 
89 
     | 
    
         
             
                  def session(env)
         
     | 
| 
       85 
90 
     | 
    
         
             
                    return env[options[:session_key]] if session? env
         
     | 
| 
       86 
     | 
    
         
            -
             
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                    raise "you need to set up a session middleware *before* #{self.class}"
         
     | 
| 
       87 
93 
     | 
    
         
             
                  end
         
     | 
| 
       88 
94 
     | 
    
         | 
| 
       89 
95 
     | 
    
         
             
                  def drop_session(env)
         
     | 
| 
         @@ -92,7 +98,8 @@ module Rack 
     | 
|
| 
       92 
98 
     | 
    
         | 
| 
       93 
99 
     | 
    
         
             
                  def referrer(env)
         
     | 
| 
       94 
100 
     | 
    
         
             
                    ref = env['HTTP_REFERER'].to_s
         
     | 
| 
       95 
     | 
    
         
            -
                    return if !options[:allow_empty_referrer]  
     | 
| 
      
 101 
     | 
    
         
            +
                    return if !options[:allow_empty_referrer] && ref.empty?
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
       96 
103 
     | 
    
         
             
                    URI.parse(ref).host || Request.new(env).host
         
     | 
| 
       97 
104 
     | 
    
         
             
                  rescue URI::InvalidURIError
         
     | 
| 
       98 
105 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -102,7 +109,7 @@ module Rack 
     | 
|
| 
       102 
109 
     | 
    
         
             
                  end
         
     | 
| 
       103 
110 
     | 
    
         | 
| 
       104 
111 
     | 
    
         
             
                  def random_string(secure = defined? SecureRandom)
         
     | 
| 
       105 
     | 
    
         
            -
                    secure ? SecureRandom.hex(16) :  
     | 
| 
      
 112 
     | 
    
         
            +
                    secure ? SecureRandom.hex(16) : '%032x' % rand((2**128) - 1)
         
     | 
| 
       106 
113 
     | 
    
         
             
                  rescue NotImplementedError
         
     | 
| 
       107 
114 
     | 
    
         
             
                    random_string false
         
     | 
| 
       108 
115 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -118,8 +125,9 @@ module Rack 
     | 
|
| 
       118 
125 
     | 
    
         
             
                  alias default_reaction deny
         
     | 
| 
       119 
126 
     | 
    
         | 
| 
       120 
127 
     | 
    
         
             
                  def html?(headers)
         
     | 
| 
       121 
     | 
    
         
            -
                    return false unless header = headers.detect { |k, 
     | 
| 
       122 
     | 
    
         
            -
             
     | 
| 
      
 128 
     | 
    
         
            +
                    return false unless (header = headers.detect { |k, _v| k.downcase == 'content-type' })
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                    options[:html_types].include? header.last[%r{^\w+/\w+}]
         
     | 
| 
       123 
131 
     | 
    
         
             
                  end
         
     | 
| 
       124 
132 
     | 
    
         
             
                end
         
     | 
| 
       125 
133 
     | 
    
         
             
              end
         
     | 
| 
         @@ -1,4 +1,5 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            #  
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
       2 
3 
     | 
    
         
             
            require 'rack/protection'
         
     | 
| 
       3 
4 
     | 
    
         | 
| 
       4 
5 
     | 
    
         
             
            module Rack
         
     | 
| 
         @@ -36,19 +37,18 @@ module Rack 
     | 
|
| 
       36 
37 
     | 
    
         
             
                #          to be used in a policy.
         
     | 
| 
       37 
38 
     | 
    
         
             
                #
         
     | 
| 
       38 
39 
     | 
    
         
             
                class ContentSecurityPolicy < Base
         
     | 
| 
       39 
     | 
    
         
            -
                  default_options default_src:  
     | 
| 
       40 
     | 
    
         
            -
                                  img_src: "'self'", style_src: "'self'",
         
     | 
| 
       41 
     | 
    
         
            -
                                  connect_src: "'self'", report_only: false
         
     | 
| 
      
 40 
     | 
    
         
            +
                  default_options default_src: "'self'", report_only: false
         
     | 
| 
       42 
41 
     | 
    
         | 
| 
       43 
     | 
    
         
            -
                  DIRECTIVES = %i 
     | 
| 
      
 42 
     | 
    
         
            +
                  DIRECTIVES = %i[base_uri child_src connect_src default_src
         
     | 
| 
       44 
43 
     | 
    
         
             
                                  font_src form_action frame_ancestors frame_src
         
     | 
| 
       45 
44 
     | 
    
         
             
                                  img_src manifest_src media_src object_src
         
     | 
| 
       46 
45 
     | 
    
         
             
                                  plugin_types referrer reflected_xss report_to
         
     | 
| 
       47 
46 
     | 
    
         
             
                                  report_uri require_sri_for sandbox script_src
         
     | 
| 
       48 
     | 
    
         
            -
                                  style_src worker_src 
     | 
| 
      
 47 
     | 
    
         
            +
                                  style_src worker_src webrtc_src navigate_to
         
     | 
| 
      
 48 
     | 
    
         
            +
                                  prefetch_src].freeze
         
     | 
| 
       49 
49 
     | 
    
         | 
| 
       50 
     | 
    
         
            -
                  NO_ARG_DIRECTIVES = %i 
     | 
| 
       51 
     | 
    
         
            -
                                         upgrade_insecure_requests 
     | 
| 
      
 50 
     | 
    
         
            +
                  NO_ARG_DIRECTIVES = %i[block_all_mixed_content disown_opener
         
     | 
| 
      
 51 
     | 
    
         
            +
                                         upgrade_insecure_requests].freeze
         
     | 
| 
       52 
52 
     | 
    
         | 
| 
       53 
53 
     | 
    
         
             
                  def csp_policy
         
     | 
| 
       54 
54 
     | 
    
         
             
                    directives = []
         
     | 
| 
         @@ -62,7 +62,7 @@ module Rack 
     | 
|
| 
       62 
62 
     | 
    
         
             
                    # Set these key values to boolean 'true' to include in policy
         
     | 
| 
       63 
63 
     | 
    
         
             
                    NO_ARG_DIRECTIVES.each do |d|
         
     | 
| 
       64 
64 
     | 
    
         
             
                      if options.key?(d) && options[d].is_a?(TrueClass)
         
     | 
| 
       65 
     | 
    
         
            -
                        directives << d.to_s. 
     | 
| 
      
 65 
     | 
    
         
            +
                        directives << d.to_s.tr('_', '-')
         
     | 
| 
       66 
66 
     | 
    
         
             
                      end
         
     | 
| 
       67 
67 
     | 
    
         
             
                    end
         
     | 
| 
       68 
68 
     | 
    
         | 
| 
         @@ -1,3 +1,5 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
       1 
3 
     | 
    
         
             
            require 'rack/protection'
         
     | 
| 
       2 
4 
     | 
    
         
             
            require 'pathname'
         
     | 
| 
       3 
5 
     | 
    
         | 
| 
         @@ -29,9 +31,8 @@ module Rack 
     | 
|
| 
       29 
31 
     | 
    
         
             
                    cookie_header = env['HTTP_COOKIE']
         
     | 
| 
       30 
32 
     | 
    
         
             
                    cookies = Rack::Utils.parse_query(cookie_header, ';,') { |s| s }
         
     | 
| 
       31 
33 
     | 
    
         
             
                    cookies.each do |k, v|
         
     | 
| 
       32 
     | 
    
         
            -
                      if k == session_key && Array(v).size > 1
         
     | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
                      elsif k != session_key && Rack::Utils.unescape(k) == session_key
         
     | 
| 
      
 34 
     | 
    
         
            +
                      if (k == session_key && Array(v).size > 1) ||
         
     | 
| 
      
 35 
     | 
    
         
            +
                         (k != session_key && Rack::Utils.unescape(k) == session_key)
         
     | 
| 
       35 
36 
     | 
    
         
             
                        bad_cookies << k
         
     | 
| 
       36 
37 
     | 
    
         
             
                      end
         
     | 
| 
       37 
38 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -40,6 +41,7 @@ module Rack 
     | 
|
| 
       40 
41 
     | 
    
         | 
| 
       41 
42 
     | 
    
         
             
                  def remove_bad_cookies(request, response)
         
     | 
| 
       42 
43 
     | 
    
         
             
                    return if bad_cookies.empty?
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
       43 
45 
     | 
    
         
             
                    paths = cookie_paths(request.path)
         
     | 
| 
       44 
46 
     | 
    
         
             
                    bad_cookies.each do |name|
         
     | 
| 
       45 
47 
     | 
    
         
             
                      paths.each { |path| response.set_cookie name, empty_cookie(request.host, path) }
         
     | 
| 
         @@ -49,7 +51,7 @@ module Rack 
     | 
|
| 
       49 
51 
     | 
    
         
             
                  def redirect(env)
         
     | 
| 
       50 
52 
     | 
    
         
             
                    request = Request.new(env)
         
     | 
| 
       51 
53 
     | 
    
         
             
                    warn env, "attack prevented by #{self.class}"
         
     | 
| 
       52 
     | 
    
         
            -
                    [302, {'Content-Type' => 'text/html', 'Location' => request.path}, []]
         
     | 
| 
      
 54 
     | 
    
         
            +
                    [302, { 'Content-Type' => 'text/html', 'Location' => request.path }, []]
         
     | 
| 
       53 
55 
     | 
    
         
             
                  end
         
     | 
| 
       54 
56 
     | 
    
         | 
| 
       55 
57 
     | 
    
         
             
                  def bad_cookies
         
     | 
| 
         @@ -64,7 +66,7 @@ module Rack 
     | 
|
| 
       64 
66 
     | 
    
         
             
                  end
         
     | 
| 
       65 
67 
     | 
    
         | 
| 
       66 
68 
     | 
    
         
             
                  def empty_cookie(host, path)
         
     | 
| 
       67 
     | 
    
         
            -
                    {: 
     | 
| 
      
 69 
     | 
    
         
            +
                    { value: '', domain: host, path: path, expires: Time.at(0) }
         
     | 
| 
       68 
70 
     | 
    
         
             
                  end
         
     | 
| 
       69 
71 
     | 
    
         | 
| 
       70 
72 
     | 
    
         
             
                  def session_key
         
     |