rack-protection 2.2.2 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa9d7fd8b6dd44cfd54fd833a1679efedfccdf76619d0b01e0aa2057f2e8b586
4
- data.tar.gz: f0541b4bf7e3cc865bdca886de807968787e90a26ca4fcd3ff3787496e64a9de
3
+ metadata.gz: 7640a15f8659807abd53474e7ce538a42e476e4bd99dc745f3b9b8c16161c008
4
+ data.tar.gz: '05468ec6c8113d3afce2df62221e4c866616999700c30ba3ef94a2705b11138b'
5
5
  SHA512:
6
- metadata.gz: 89f4d7bd2b9ee4a1e51fb9a7d9699f0d1d3881a82bb3b8813527f6ce8cce96274462fd502e8df4d5f6b21cbf2d79da9bd69bff6a4e6e7e289e69aa7cb4e18893
7
- data.tar.gz: 381cdab10dffb59181caef1a80b46e9d584e1f20fa602843f1649164bbd16d44cbfd5ba5aef2ebd0a447e52fad7437644d29188e98b9998a19a4377abbc0051f
6
+ metadata.gz: eeaff5e584a8ee3be6c80dc92c67fcc95bdbb97b084509ed90ca9ad524598fba63690cfa372586edd27940cd609fa44210637e9c95fbf1191e1a5cc297f222ac
7
+ data.tar.gz: 26e2160d65b6015c7aaa52266b7241d15f645eb259d1371b864b3e2b6a3b1fbef841e62304bc8e39a83fab1a52ddb0c3455a51385a9a451c833cbed91b75d00a
data/Gemfile CHANGED
@@ -1,13 +1,16 @@
1
- source "https://rubygems.org"
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
2
4
  # encoding: utf-8
3
5
 
4
6
  gem 'rake'
7
+ gem 'rspec', '~> 3'
5
8
 
6
9
  rack_version = ENV['rack'].to_s
7
- rack_version = nil if rack_version.empty? or rack_version == 'stable'
8
- rack_version = {:github => 'rack/rack'} if rack_version == 'master'
10
+ rack_version = nil if rack_version.empty? || (rack_version == 'stable')
11
+ rack_version = { github: 'rack/rack' } if rack_version == 'head'
9
12
  gem 'rack', rack_version
10
13
 
11
- gem 'sinatra', path: '..'
12
-
13
14
  gemspec
15
+
16
+ gem 'rack-test'
data/README.md CHANGED
@@ -74,6 +74,7 @@ Prevented by:
74
74
  ## Cookie Tossing
75
75
 
76
76
  Prevented by:
77
+
77
78
  * [`Rack::Protection::CookieTossing`][cookie-tossing] (not included by `use Rack::Protection`)
78
79
 
79
80
  ## IP Spoofing
@@ -95,6 +96,7 @@ Prevented by:
95
96
  # Instrumentation
96
97
 
97
98
  Instrumentation is enabled by passing in an instrumenter as an option.
99
+
98
100
  ```
99
101
  use Rack::Protection, instrumenter: ActiveSupport::Notifications
100
102
  ```
data/Rakefile CHANGED
@@ -1,53 +1,55 @@
1
- # encoding: utf-8
2
- $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
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
- $stderr.puts e
9
+ warn e
9
10
  end
10
11
 
11
- desc "run specs"
12
- task(:spec) { ruby '-S rspec spec' }
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("/_", "-")}.rdoc"
21
- Dir.mkdir "doc" unless File.directory? "doc"
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, "w") { |f| f << doc }
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("README.md")
29
- file = "doc/rack-protection-readme.md"
30
- Dir.mkdir "doc" unless File.directory? "doc"
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, "w") { |f| f << doc }
34
+ File.open(file, 'w') { |f| f << doc }
33
35
  end
34
36
 
35
- task :all => [:readmes, :index]
37
+ task all: %i[readmes index]
36
38
  end
37
39
 
38
- desc "generate documentation"
39
- task :doc => 'doc:all'
40
+ desc 'generate documentation'
41
+ task doc: 'doc:all'
40
42
 
41
- desc "generate gemspec"
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
- :authors => `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
49
- :email => ["mail@zzak.io", "konstantin.haase@gmail.com"],
50
- :files => %w(License README.md Rakefile Gemfile rack-protection.gemspec) + Dir['lib/**/*']
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 :gemspec => 'rack-protection.gemspec'
71
- task :default => :spec
72
- task :test => :spec
72
+ task gemspec: 'rack-protection.gemspec'
73
+ task default: :spec
74
+ task test: :spec
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/protection'
2
4
  require 'securerandom'
3
5
  require 'openssl'
@@ -17,7 +19,10 @@ module Rack
17
19
  # It checks the <tt>X-CSRF-Token</tt> header and the <tt>POST</tt> form
18
20
  # data.
19
21
  #
20
- # Compatible with the {rack-csrf}[https://rubygems.org/gems/rack_csrf] gem.
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")
21
26
  #
22
27
  # == Options
23
28
  #
@@ -92,12 +97,12 @@ module Rack
92
97
  class AuthenticityToken < Base
93
98
  TOKEN_LENGTH = 32
94
99
 
95
- default_options :authenticity_param => 'authenticity_token',
96
- :key => :csrf,
97
- :allow_if => nil
100
+ default_options authenticity_param: 'authenticity_token',
101
+ key: :csrf,
102
+ allow_if: nil
98
103
 
99
104
  def self.token(session, path: nil, method: :post)
100
- self.new(nil).mask_authenticity_token(session, path: path, method: method)
105
+ new(nil).mask_authenticity_token(session, path: path, method: method)
101
106
  end
102
107
 
103
108
  def self.random_token
@@ -111,8 +116,8 @@ module Rack
111
116
  safe?(env) ||
112
117
  valid_token?(env, env['HTTP_X_CSRF_TOKEN']) ||
113
118
  valid_token?(env, Request.new(env).params[options[:authenticity_param]]) ||
114
- ( options[:allow_if] && options[:allow_if].call(env) )
115
- rescue
119
+ options[:allow_if]&.call(env)
120
+ rescue StandardError
116
121
  false
117
122
  end
118
123
 
@@ -120,10 +125,10 @@ module Rack
120
125
  set_token(session)
121
126
 
122
127
  token = if path && method
123
- per_form_token(session, path, method)
124
- else
125
- global_token(session)
126
- end
128
+ per_form_token(session, path, method)
129
+ else
130
+ global_token(session)
131
+ end
127
132
 
128
133
  mask_token(token)
129
134
  end
@@ -140,7 +145,7 @@ module Rack
140
145
  # Checks the client's masked token to see if it matches the
141
146
  # session token.
142
147
  def valid_token?(env, token)
143
- return false if token.nil? || token.empty?
148
+ return false if token.nil? || !token.is_a?(String) || token.empty?
144
149
 
145
150
  session = session(env)
146
151
 
@@ -182,7 +187,7 @@ module Rack
182
187
  # value and decrypt it
183
188
  token_length = masked_token.length / 2
184
189
  one_time_pad = masked_token[0...token_length]
185
- encrypted_token = masked_token[token_length..-1]
190
+ encrypted_token = masked_token[token_length..]
186
191
  xor_byte_strings(one_time_pad, encrypted_token)
187
192
  end
188
193
 
@@ -204,8 +209,7 @@ module Rack
204
209
 
205
210
  def compare_with_per_form_token(token, session, request)
206
211
  secure_compare(token,
207
- per_form_token(session, request.path.chomp('/'), request.request_method)
208
- )
212
+ per_form_token(session, request.path.chomp('/'), request.request_method))
209
213
  end
210
214
 
211
215
  def real_token(session)
@@ -230,7 +234,7 @@ module Rack
230
234
 
231
235
  def token_hmac(session, identifier)
232
236
  OpenSSL::HMAC.digest(
233
- OpenSSL::Digest::SHA256.new,
237
+ OpenSSL::Digest.new('SHA256'),
234
238
  real_token(session),
235
239
  identifier
236
240
  )
@@ -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
- :reaction => :default_reaction, :logging => true,
12
- :message => 'Forbidden', :encryptor => Digest::SHA1,
13
- :session_key => 'rack.session', :status => 403,
14
- :allow_empty_referrer => true,
15
- :report_key => "protection.failed",
16
- :html_types => %w[text/html application/xhtml text/xml application/xml]
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, @options = app, default_options.merge(options)
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 and result.size == 3
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,16 +88,24 @@ module Rack
83
88
 
84
89
  def session(env)
85
90
  return env[options[:session_key]] if session? env
86
- fail "you need to set up a session middleware *before* #{self.class}"
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)
90
- session(env).clear if session? env
96
+ return unless session? env
97
+
98
+ session(env).clear
99
+
100
+ return if ["1", "true"].include?(ENV["RACK_PROTECTION_SILENCE_DROP_SESSION_WARNING"])
101
+
102
+ warn env, "session dropped by #{self.class}"
91
103
  end
92
104
 
93
105
  def referrer(env)
94
106
  ref = env['HTTP_REFERER'].to_s
95
- return if !options[:allow_empty_referrer] and ref.empty?
107
+ return if !options[:allow_empty_referrer] && ref.empty?
108
+
96
109
  URI.parse(ref).host || Request.new(env).host
97
110
  rescue URI::InvalidURIError
98
111
  end
@@ -102,7 +115,7 @@ module Rack
102
115
  end
103
116
 
104
117
  def random_string(secure = defined? SecureRandom)
105
- secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
118
+ secure ? SecureRandom.hex(16) : '%032x' % rand((2**128) - 1)
106
119
  rescue NotImplementedError
107
120
  random_string false
108
121
  end
@@ -118,8 +131,9 @@ module Rack
118
131
  alias default_reaction deny
119
132
 
120
133
  def html?(headers)
121
- return false unless header = headers.detect { |k,v| k.downcase == 'content-type' }
122
- options[:html_types].include? header.last[/^\w+\/\w+/]
134
+ return false unless (header = headers.detect { |k, _v| k.downcase == 'content-type' })
135
+
136
+ options[:html_types].include? header.last[%r{^\w+/\w+}]
123
137
  end
124
138
  end
125
139
  end
@@ -1,4 +1,5 @@
1
- # -*- coding: utf-8 -*-
1
+ # frozen_string_literal: true
2
+
2
3
  require 'rack/protection'
3
4
 
4
5
  module Rack
@@ -38,16 +39,16 @@ module Rack
38
39
  class ContentSecurityPolicy < Base
39
40
  default_options default_src: "'self'", report_only: false
40
41
 
41
- DIRECTIVES = %i(base_uri child_src connect_src default_src
42
+ DIRECTIVES = %i[base_uri child_src connect_src default_src
42
43
  font_src form_action frame_ancestors frame_src
43
44
  img_src manifest_src media_src object_src
44
45
  plugin_types referrer reflected_xss report_to
45
46
  report_uri require_sri_for sandbox script_src
46
47
  style_src worker_src webrtc_src navigate_to
47
- prefetch_src).freeze
48
+ prefetch_src].freeze
48
49
 
49
- NO_ARG_DIRECTIVES = %i(block_all_mixed_content disown_opener
50
- upgrade_insecure_requests).freeze
50
+ NO_ARG_DIRECTIVES = %i[block_all_mixed_content disown_opener
51
+ upgrade_insecure_requests].freeze
51
52
 
52
53
  def csp_policy
53
54
  directives = []
@@ -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
- bad_cookies << k
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
- {:value => '', :domain => host, :path => path, :expires => Time.at(0)}
69
+ { value: '', domain: host, path: path, expires: Time.at(0) }
68
70
  end
69
71
 
70
72
  def session_key