rack-protection 2.1.0 → 4.1.1
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 +8 -8
- data/README.md +8 -1
- data/Rakefile +24 -22
- data/lib/rack/protection/authenticity_token.rb +81 -28
- data/lib/rack/protection/base.rb +37 -16
- data/lib/rack/protection/content_security_policy.rb +8 -7
- data/lib/rack/protection/cookie_tossing.rb +7 -5
- 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 +4 -2
- data/lib/rack/protection/host_authorization.rb +110 -0
- data/lib/rack/protection/http_origin.rb +6 -10
- 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 +8 -5
- data/lib/rack/protection/referrer_policy.rb +4 -2
- 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 +5 -3
- data/lib/rack/protection/version.rb +3 -1
- data/lib/rack/protection/xss_header.rb +5 -3
- data/lib/rack/protection.rb +5 -3
- data/lib/rack-protection.rb +1 -1
- data/lib/rack_protection.rb +3 -0
- data/rack-protection.gemspec +30 -25
- metadata +29 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98553e74b3c41ed1630ad332c07ac4be4d2cf7f9f7d42c4ea383ed10b36cfda8
|
4
|
+
data.tar.gz: d1af8ebd50af6447a3879ae531de1dd5e664cb7493919f2af94c4d2a5d0036d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c38bcefaa24abe73b6d19562e3ac028aa857458ba9a60230e94b383c94b5493e1e0488ec08f4afdd6d09fd4a6cf088662be0f48e21da55cbadf45eaf1599f47
|
7
|
+
data.tar.gz: ddaf456e0d4d72f433f1d08f7dcf0c12280b228f43aaca4119ed60201170bcf57ba3cb9383be0b86e179ac664b568bb7fa37dbb5695d6f2a8583e8edc8b706c7
|
data/Gemfile
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
gemspec
|
3
5
|
|
4
6
|
gem 'rake'
|
7
|
+
gem 'rspec', '~> 3'
|
8
|
+
gem 'rack-test'
|
5
9
|
|
6
10
|
rack_version = ENV['rack'].to_s
|
7
|
-
rack_version = nil if rack_version.empty?
|
8
|
-
rack_version = {:
|
11
|
+
rack_version = nil if rack_version.empty? || (rack_version == 'stable')
|
12
|
+
rack_version = { github: 'rack/rack' } if rack_version == 'head'
|
9
13
|
gem 'rack', rack_version
|
10
|
-
|
11
|
-
gem 'sinatra', path: '..'
|
12
|
-
|
13
|
-
gemspec
|
data/README.md
CHANGED
@@ -34,6 +34,10 @@ run MyApp
|
|
34
34
|
|
35
35
|
# Prevented Attacks
|
36
36
|
|
37
|
+
## DNS rebinding and other Host header attacks
|
38
|
+
|
39
|
+
* [`Rack::Protection::HostAuthorization`][host-authorization] (not included by `use Rack::Protection`)
|
40
|
+
|
37
41
|
## Cross Site Request Forgery
|
38
42
|
|
39
43
|
Prevented by:
|
@@ -69,11 +73,12 @@ Prevented by:
|
|
69
73
|
|
70
74
|
Prevented by:
|
71
75
|
|
72
|
-
* [`Rack::Protection::SessionHijacking`][session-hijacking]
|
76
|
+
* [`Rack::Protection::SessionHijacking`][session-hijacking] (not included by `use Rack::Protection`)
|
73
77
|
|
74
78
|
## Cookie Tossing
|
75
79
|
|
76
80
|
Prevented by:
|
81
|
+
|
77
82
|
* [`Rack::Protection::CookieTossing`][cookie-tossing] (not included by `use Rack::Protection`)
|
78
83
|
|
79
84
|
## IP Spoofing
|
@@ -95,6 +100,7 @@ Prevented by:
|
|
95
100
|
# Instrumentation
|
96
101
|
|
97
102
|
Instrumentation is enabled by passing in an instrumenter as an option.
|
103
|
+
|
98
104
|
```
|
99
105
|
use Rack::Protection, instrumenter: ActiveSupport::Notifications
|
100
106
|
```
|
@@ -107,6 +113,7 @@ The instrumenter is passed a namespace (String) and environment (Hash). The name
|
|
107
113
|
[escaped-params]: http://www.sinatrarb.com/protection/escaped_params
|
108
114
|
[form-token]: http://www.sinatrarb.com/protection/form_token
|
109
115
|
[frame-options]: http://www.sinatrarb.com/protection/frame_options
|
116
|
+
[host-authorization]: https://github.com/sinatra/sinatra/blob/main/rack-protection/lib/rack/protection/host_authorization.rb
|
110
117
|
[http-origin]: http://www.sinatrarb.com/protection/http_origin
|
111
118
|
[ip-spoofing]: http://www.sinatrarb.com/protection/ip_spoofing
|
112
119
|
[json-csrf]: http://www.sinatrarb.com/protection/json_csrf
|
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
|
@@ -33,14 +46,15 @@ module Rack
|
|
33
46
|
# Install the gem, then run the program:
|
34
47
|
#
|
35
48
|
# gem install 'rack-protection'
|
36
|
-
#
|
49
|
+
# puma server.ru
|
37
50
|
#
|
38
|
-
# Here is <tt>server.
|
51
|
+
# Here is <tt>server.ru</tt>:
|
39
52
|
#
|
40
53
|
# require 'rack/protection'
|
54
|
+
# require 'rack/session'
|
41
55
|
#
|
42
56
|
# app = Rack::Builder.app do
|
43
|
-
# use Rack::Session::Cookie, secret: '
|
57
|
+
# use Rack::Session::Cookie, secret: 'CHANGEMEaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
44
58
|
# use Rack::Protection::AuthenticityToken
|
45
59
|
#
|
46
60
|
# run -> (env) do
|
@@ -74,7 +88,7 @@ module Rack
|
|
74
88
|
# end
|
75
89
|
# end
|
76
90
|
#
|
77
|
-
#
|
91
|
+
# run app
|
78
92
|
#
|
79
93
|
# == Example: Customize which POST parameter holds the token
|
80
94
|
#
|
@@ -84,42 +98,57 @@ module Rack
|
|
84
98
|
class AuthenticityToken < Base
|
85
99
|
TOKEN_LENGTH = 32
|
86
100
|
|
87
|
-
default_options :
|
88
|
-
:
|
101
|
+
default_options authenticity_param: 'authenticity_token',
|
102
|
+
key: :csrf,
|
103
|
+
allow_if: nil
|
89
104
|
|
90
|
-
def self.token(session)
|
91
|
-
|
105
|
+
def self.token(session, path: nil, method: :post)
|
106
|
+
new(nil).mask_authenticity_token(session, path: path, method: method)
|
92
107
|
end
|
93
108
|
|
94
109
|
def self.random_token
|
95
|
-
SecureRandom.
|
110
|
+
SecureRandom.urlsafe_base64(TOKEN_LENGTH, padding: false)
|
96
111
|
end
|
97
112
|
|
98
113
|
def accepts?(env)
|
99
|
-
session = session
|
114
|
+
session = session(env)
|
100
115
|
set_token(session)
|
101
116
|
|
102
117
|
safe?(env) ||
|
103
|
-
valid_token?(
|
104
|
-
valid_token?(
|
105
|
-
|
118
|
+
valid_token?(env, env['HTTP_X_CSRF_TOKEN']) ||
|
119
|
+
valid_token?(env, Request.new(env).params[options[:authenticity_param]]) ||
|
120
|
+
options[:allow_if]&.call(env)
|
121
|
+
rescue StandardError
|
122
|
+
false
|
106
123
|
end
|
107
124
|
|
108
|
-
def mask_authenticity_token(session)
|
109
|
-
|
125
|
+
def mask_authenticity_token(session, path: nil, method: :post)
|
126
|
+
set_token(session)
|
127
|
+
|
128
|
+
token = if path && method
|
129
|
+
per_form_token(session, path, method)
|
130
|
+
else
|
131
|
+
global_token(session)
|
132
|
+
end
|
133
|
+
|
110
134
|
mask_token(token)
|
111
135
|
end
|
112
136
|
|
137
|
+
GLOBAL_TOKEN_IDENTIFIER = '!real_csrf_token'
|
138
|
+
private_constant :GLOBAL_TOKEN_IDENTIFIER
|
139
|
+
|
113
140
|
private
|
114
141
|
|
115
142
|
def set_token(session)
|
116
|
-
session[:
|
143
|
+
session[options[:key]] ||= self.class.random_token
|
117
144
|
end
|
118
145
|
|
119
146
|
# Checks the client's masked token to see if it matches the
|
120
147
|
# session token.
|
121
|
-
def valid_token?(
|
122
|
-
return false if token.nil? || token.empty?
|
148
|
+
def valid_token?(env, token)
|
149
|
+
return false if token.nil? || !token.is_a?(String) || token.empty?
|
150
|
+
|
151
|
+
session = session(env)
|
123
152
|
|
124
153
|
begin
|
125
154
|
token = decode_token(token)
|
@@ -131,13 +160,13 @@ module Rack
|
|
131
160
|
# to handle any unmasked tokens that we've issued without error.
|
132
161
|
|
133
162
|
if unmasked_token?(token)
|
134
|
-
compare_with_real_token
|
135
|
-
|
163
|
+
compare_with_real_token(token, session)
|
136
164
|
elsif masked_token?(token)
|
137
165
|
token = unmask_token(token)
|
138
166
|
|
139
|
-
|
140
|
-
|
167
|
+
compare_with_global_token(token, session) ||
|
168
|
+
compare_with_real_token(token, session) ||
|
169
|
+
compare_with_per_form_token(token, session, Request.new(env))
|
141
170
|
else
|
142
171
|
false # Token is malformed
|
143
172
|
end
|
@@ -147,7 +176,6 @@ module Rack
|
|
147
176
|
# on each request. The masking is used to mitigate SSL attacks
|
148
177
|
# like BREACH.
|
149
178
|
def mask_token(token)
|
150
|
-
token = decode_token(token)
|
151
179
|
one_time_pad = SecureRandom.random_bytes(token.length)
|
152
180
|
encrypted_token = xor_byte_strings(one_time_pad, token)
|
153
181
|
masked_token = one_time_pad + encrypted_token
|
@@ -160,7 +188,7 @@ module Rack
|
|
160
188
|
# value and decrypt it
|
161
189
|
token_length = masked_token.length / 2
|
162
190
|
one_time_pad = masked_token[0...token_length]
|
163
|
-
encrypted_token = masked_token[token_length
|
191
|
+
encrypted_token = masked_token[token_length..]
|
164
192
|
xor_byte_strings(one_time_pad, encrypted_token)
|
165
193
|
end
|
166
194
|
|
@@ -176,16 +204,41 @@ module Rack
|
|
176
204
|
secure_compare(token, real_token(session))
|
177
205
|
end
|
178
206
|
|
207
|
+
def compare_with_global_token(token, session)
|
208
|
+
secure_compare(token, global_token(session))
|
209
|
+
end
|
210
|
+
|
211
|
+
def compare_with_per_form_token(token, session, request)
|
212
|
+
secure_compare(token,
|
213
|
+
per_form_token(session, request.path.chomp('/'), request.request_method))
|
214
|
+
end
|
215
|
+
|
179
216
|
def real_token(session)
|
180
|
-
decode_token(session[:
|
217
|
+
decode_token(session[options[:key]])
|
218
|
+
end
|
219
|
+
|
220
|
+
def global_token(session)
|
221
|
+
token_hmac(session, GLOBAL_TOKEN_IDENTIFIER)
|
222
|
+
end
|
223
|
+
|
224
|
+
def per_form_token(session, path, method)
|
225
|
+
token_hmac(session, "#{path}##{method.downcase}")
|
181
226
|
end
|
182
227
|
|
183
228
|
def encode_token(token)
|
184
|
-
Base64.
|
229
|
+
Base64.urlsafe_encode64(token)
|
185
230
|
end
|
186
231
|
|
187
232
|
def decode_token(token)
|
188
|
-
Base64.
|
233
|
+
Base64.urlsafe_decode64(token)
|
234
|
+
end
|
235
|
+
|
236
|
+
def token_hmac(session, identifier)
|
237
|
+
OpenSSL::HMAC.digest(
|
238
|
+
OpenSSL::Digest.new('SHA256'),
|
239
|
+
real_token(session),
|
240
|
+
identifier
|
241
|
+
)
|
189
242
|
end
|
190
243
|
|
191
244
|
def xor_byte_strings(s1, s2)
|
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,33 @@ 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)
|
59
|
+
end
|
60
|
+
|
61
|
+
def debug(env, message)
|
62
|
+
return unless options[:logging]
|
63
|
+
|
64
|
+
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
|
65
|
+
l.debug(message)
|
56
66
|
end
|
57
67
|
|
58
68
|
def warn(env, message)
|
59
69
|
return unless options[:logging]
|
70
|
+
|
60
71
|
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
|
61
72
|
l.warn(message)
|
62
73
|
end
|
63
74
|
|
64
75
|
def instrument(env)
|
65
|
-
return unless i = options[:instrumenter]
|
76
|
+
return unless (i = options[:instrumenter])
|
77
|
+
|
66
78
|
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
|
67
79
|
i.instrument('rack.protection', env)
|
68
80
|
end
|
69
81
|
|
70
82
|
def deny(env)
|
71
83
|
warn env, "attack prevented by #{self.class}"
|
72
|
-
[options[:status], {'
|
84
|
+
[options[:status], { 'content-type' => 'text/plain' }, [options[:message]]]
|
73
85
|
end
|
74
86
|
|
75
87
|
def report(env)
|
@@ -83,16 +95,24 @@ module Rack
|
|
83
95
|
|
84
96
|
def session(env)
|
85
97
|
return env[options[:session_key]] if session? env
|
86
|
-
|
98
|
+
|
99
|
+
raise "you need to set up a session middleware *before* #{self.class}"
|
87
100
|
end
|
88
101
|
|
89
102
|
def drop_session(env)
|
90
|
-
|
103
|
+
return unless session? env
|
104
|
+
|
105
|
+
session(env).clear
|
106
|
+
|
107
|
+
return if ["1", "true"].include?(ENV["RACK_PROTECTION_SILENCE_DROP_SESSION_WARNING"])
|
108
|
+
|
109
|
+
warn env, "session dropped by #{self.class}"
|
91
110
|
end
|
92
111
|
|
93
112
|
def referrer(env)
|
94
113
|
ref = env['HTTP_REFERER'].to_s
|
95
|
-
return if !options[:allow_empty_referrer]
|
114
|
+
return if !options[:allow_empty_referrer] && ref.empty?
|
115
|
+
|
96
116
|
URI.parse(ref).host || Request.new(env).host
|
97
117
|
rescue URI::InvalidURIError
|
98
118
|
end
|
@@ -102,7 +122,7 @@ module Rack
|
|
102
122
|
end
|
103
123
|
|
104
124
|
def random_string(secure = defined? SecureRandom)
|
105
|
-
secure ? SecureRandom.hex(16) :
|
125
|
+
secure ? SecureRandom.hex(16) : '%032x' % rand((2**128) - 1)
|
106
126
|
rescue NotImplementedError
|
107
127
|
random_string false
|
108
128
|
end
|
@@ -118,8 +138,9 @@ module Rack
|
|
118
138
|
alias default_reaction deny
|
119
139
|
|
120
140
|
def html?(headers)
|
121
|
-
return false unless header = headers.detect { |k,
|
122
|
-
|
141
|
+
return false unless (header = headers.detect { |k, _v| k.downcase == 'content-type' })
|
142
|
+
|
143
|
+
options[:html_types].include? header.last[%r{^\w+/\w+}]
|
123
144
|
end
|
124
145
|
end
|
125
146
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'rack/protection'
|
3
4
|
|
4
5
|
module Rack
|
@@ -25,7 +26,7 @@ module Rack
|
|
25
26
|
# https://scotthelme.co.uk/csp-cheat-sheet/
|
26
27
|
# http://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
27
28
|
#
|
28
|
-
# Sets the '
|
29
|
+
# Sets the 'content-security-policy[-report-only]' header.
|
29
30
|
#
|
30
31
|
# Options: ContentSecurityPolicy configuration is a complex topic with
|
31
32
|
# several levels of support that has evolved over time.
|
@@ -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
|
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
|
48
|
+
prefetch_src].freeze
|
48
49
|
|
49
|
-
NO_ARG_DIRECTIVES = %i
|
50
|
-
upgrade_insecure_requests
|
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 = []
|
@@ -70,7 +71,7 @@ module Rack
|
|
70
71
|
|
71
72
|
def call(env)
|
72
73
|
status, headers, body = @app.call(env)
|
73
|
-
header = options[:report_only] ? '
|
74
|
+
header = options[:report_only] ? 'content-security-policy-report-only' : 'content-security-policy'
|
74
75
|
headers[header] ||= csp_policy if html? headers
|
75
76
|
[status, headers, body]
|
76
77
|
end
|
@@ -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, {'
|
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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
require 'rack/utils'
|
3
5
|
require 'tempfile'
|
@@ -15,8 +17,7 @@ module Rack
|
|
15
17
|
# More infos:: http://en.wikipedia.org/wiki/Cross-site_scripting
|
16
18
|
#
|
17
19
|
# Automatically escapes Rack::Request#params so they can be embedded in HTML
|
18
|
-
# or JavaScript without any further issues.
|
19
|
-
# strings if defined, to avoid double-escaping in Rails.
|
20
|
+
# or JavaScript without any further issues.
|
20
21
|
#
|
21
22
|
# Options:
|
22
23
|
# escape:: What escaping modes to use, should be Symbol or Array of Symbols.
|
@@ -29,8 +30,8 @@ module Rack
|
|
29
30
|
public :escape_html
|
30
31
|
end
|
31
32
|
|
32
|
-
default_options :
|
33
|
-
|
33
|
+
default_options escape: :html,
|
34
|
+
escaper: defined?(EscapeUtils) ? EscapeUtils : self
|
34
35
|
|
35
36
|
def initialize(*)
|
36
37
|
super
|
@@ -41,15 +42,19 @@ module Rack
|
|
41
42
|
@javascript = modes.include? :javascript
|
42
43
|
@url = modes.include? :url
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
45
|
+
return unless @javascript && (!@escaper.respond_to? :escape_javascript)
|
46
|
+
|
47
|
+
raise('Use EscapeUtils for JavaScript escaping.')
|
47
48
|
end
|
48
49
|
|
49
50
|
def call(env)
|
50
51
|
request = Request.new(env)
|
51
52
|
get_was = handle(request.GET)
|
52
|
-
post_was =
|
53
|
+
post_was = begin
|
54
|
+
handle(request.POST)
|
55
|
+
rescue StandardError
|
56
|
+
nil
|
57
|
+
end
|
53
58
|
app.call env
|
54
59
|
ensure
|
55
60
|
request.GET.replace get_was if get_was
|
@@ -68,13 +73,12 @@ module Rack
|
|
68
73
|
when Array then object.map { |o| escape(o) }
|
69
74
|
when String then escape_string(object)
|
70
75
|
when Tempfile then object
|
71
|
-
else nil
|
72
76
|
end
|
73
77
|
end
|
74
78
|
|
75
79
|
def escape_hash(hash)
|
76
80
|
hash = hash.dup
|
77
|
-
hash.each { |k,v| hash[k] = escape(v) }
|
81
|
+
hash.each { |k, v| hash[k] = escape(v) }
|
78
82
|
hash
|
79
83
|
end
|
80
84
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -16,7 +18,7 @@ module Rack
|
|
16
18
|
# Compatible with rack-csrf.
|
17
19
|
class FormToken < AuthenticityToken
|
18
20
|
def accepts?(env)
|
19
|
-
env[
|
21
|
+
env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' or super
|
20
22
|
end
|
21
23
|
end
|
22
24
|
end
|