rack-protection 2.2.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +8 -8
- data/README.md +3 -1
- data/Rakefile +24 -22
- data/lib/rack/protection/authenticity_token.rb +21 -16
- data/lib/rack/protection/base.rb +30 -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/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 +4 -3
- data/lib/rack-protection.rb +1 -1
- data/lib/rack_protection.rb +3 -0
- data/rack-protection.gemspec +29 -25
- metadata +24 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8762f8b7bc260c94e353c6c9eb9a24d3a0efe695cb5904f526a5a84094f567db
|
4
|
+
data.tar.gz: d3c7e05bf8bfea7c6b077025f330429f386b416bccce5ebfab2d91d0513e437b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48b9fffade45349249d1122c3acc8618f2cb8ca05cde4ce1959b063899faad7c5c433bf6481518beedd0a0dd2ff28c878b1f5898fc3f21629ea39233fa747dc6
|
7
|
+
data.tar.gz: ec3176d39b37dfdd97b4d230f98251681a93c6dddcb17db4659c998d8f296dc9fc97808a9b732bbf055d0d2b0504f9696f541b6d6864983b214df94ceff636a5
|
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
@@ -69,11 +69,12 @@ Prevented by:
|
|
69
69
|
|
70
70
|
Prevented by:
|
71
71
|
|
72
|
-
* [`Rack::Protection::SessionHijacking`][session-hijacking]
|
72
|
+
* [`Rack::Protection::SessionHijacking`][session-hijacking] (not included by `use Rack::Protection`)
|
73
73
|
|
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
|
-
#
|
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,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
|
-
#
|
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
|
#
|
@@ -46,6 +51,7 @@ module Rack
|
|
46
51
|
# Here is <tt>server.rb</tt>:
|
47
52
|
#
|
48
53
|
# require 'rack/protection'
|
54
|
+
# require 'rack/session'
|
49
55
|
#
|
50
56
|
# app = Rack::Builder.app do
|
51
57
|
# use Rack::Session::Cookie, secret: 'secret'
|
@@ -92,12 +98,12 @@ module Rack
|
|
92
98
|
class AuthenticityToken < Base
|
93
99
|
TOKEN_LENGTH = 32
|
94
100
|
|
95
|
-
default_options :
|
96
|
-
:
|
97
|
-
:
|
101
|
+
default_options authenticity_param: 'authenticity_token',
|
102
|
+
key: :csrf,
|
103
|
+
allow_if: nil
|
98
104
|
|
99
105
|
def self.token(session, path: nil, method: :post)
|
100
|
-
|
106
|
+
new(nil).mask_authenticity_token(session, path: path, method: method)
|
101
107
|
end
|
102
108
|
|
103
109
|
def self.random_token
|
@@ -111,8 +117,8 @@ module Rack
|
|
111
117
|
safe?(env) ||
|
112
118
|
valid_token?(env, env['HTTP_X_CSRF_TOKEN']) ||
|
113
119
|
valid_token?(env, Request.new(env).params[options[:authenticity_param]]) ||
|
114
|
-
|
115
|
-
rescue
|
120
|
+
options[:allow_if]&.call(env)
|
121
|
+
rescue StandardError
|
116
122
|
false
|
117
123
|
end
|
118
124
|
|
@@ -120,10 +126,10 @@ module Rack
|
|
120
126
|
set_token(session)
|
121
127
|
|
122
128
|
token = if path && method
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
129
|
+
per_form_token(session, path, method)
|
130
|
+
else
|
131
|
+
global_token(session)
|
132
|
+
end
|
127
133
|
|
128
134
|
mask_token(token)
|
129
135
|
end
|
@@ -140,7 +146,7 @@ module Rack
|
|
140
146
|
# Checks the client's masked token to see if it matches the
|
141
147
|
# session token.
|
142
148
|
def valid_token?(env, token)
|
143
|
-
return false if token.nil? || token.empty?
|
149
|
+
return false if token.nil? || !token.is_a?(String) || token.empty?
|
144
150
|
|
145
151
|
session = session(env)
|
146
152
|
|
@@ -182,7 +188,7 @@ module Rack
|
|
182
188
|
# value and decrypt it
|
183
189
|
token_length = masked_token.length / 2
|
184
190
|
one_time_pad = masked_token[0...token_length]
|
185
|
-
encrypted_token = masked_token[token_length
|
191
|
+
encrypted_token = masked_token[token_length..]
|
186
192
|
xor_byte_strings(one_time_pad, encrypted_token)
|
187
193
|
end
|
188
194
|
|
@@ -204,8 +210,7 @@ module Rack
|
|
204
210
|
|
205
211
|
def compare_with_per_form_token(token, session, request)
|
206
212
|
secure_compare(token,
|
207
|
-
|
208
|
-
)
|
213
|
+
per_form_token(session, request.path.chomp('/'), request.request_method))
|
209
214
|
end
|
210
215
|
|
211
216
|
def real_token(session)
|
@@ -230,7 +235,7 @@ module Rack
|
|
230
235
|
|
231
236
|
def token_hmac(session, identifier)
|
232
237
|
OpenSSL::HMAC.digest(
|
233
|
-
OpenSSL::Digest
|
238
|
+
OpenSSL::Digest.new('SHA256'),
|
234
239
|
real_token(session),
|
235
240
|
identifier
|
236
241
|
)
|
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], {'
|
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
|
-
|
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
|
-
|
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]
|
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) :
|
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,
|
122
|
-
|
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
|
-
#
|
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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -17,7 +19,7 @@ module Rack
|
|
17
19
|
# frame. Use :deny to forbid any embedding, :sameorigin
|
18
20
|
# to allow embedding from the same origin (default).
|
19
21
|
class FrameOptions < Base
|
20
|
-
default_options :
|
22
|
+
default_options frame_options: :sameorigin
|
21
23
|
|
22
24
|
def frame_options
|
23
25
|
@frame_options ||= begin
|
@@ -29,7 +31,7 @@ module Rack
|
|
29
31
|
|
30
32
|
def call(env)
|
31
33
|
status, headers, body = @app.call(env)
|
32
|
-
headers['
|
34
|
+
headers['x-frame-options'] ||= frame_options if html? headers
|
33
35
|
[status, headers, body]
|
34
36
|
end
|
35
37
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -19,7 +21,7 @@ module Rack
|
|
19
21
|
class HttpOrigin < Base
|
20
22
|
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
|
21
23
|
default_reaction :deny
|
22
|
-
default_options :
|
24
|
+
default_options allow_if: nil
|
23
25
|
|
24
26
|
def base_url(env)
|
25
27
|
request = Rack::Request.new(env)
|
@@ -29,19 +31,13 @@ module Rack
|
|
29
31
|
|
30
32
|
def accepts?(env)
|
31
33
|
return true if safe? env
|
32
|
-
return true unless origin = env['HTTP_ORIGIN']
|
34
|
+
return true unless (origin = env['HTTP_ORIGIN'])
|
33
35
|
return true if base_url(env) == origin
|
34
|
-
return true if options[:allow_if]
|
35
|
-
|
36
|
-
if options.key? :origin_whitelist
|
37
|
-
warn env, "Rack::Protection origin_whitelist option is deprecated and will be removed, " \
|
38
|
-
"use permitted_origins instead.\n"
|
39
|
-
end
|
36
|
+
return true if options[:allow_if]&.call(env)
|
40
37
|
|
41
|
-
permitted_origins = options[:permitted_origins]
|
38
|
+
permitted_origins = options[:permitted_origins]
|
42
39
|
Array(permitted_origins).include? origin
|
43
40
|
end
|
44
|
-
|
45
41
|
end
|
46
42
|
end
|
47
43
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -13,9 +15,11 @@ module Rack
|
|
13
15
|
|
14
16
|
def accepts?(env)
|
15
17
|
return true unless env.include? 'HTTP_X_FORWARDED_FOR'
|
16
|
-
|
17
|
-
|
18
|
-
return false if env.include?
|
18
|
+
|
19
|
+
ips = env['HTTP_X_FORWARDED_FOR'].split(',').map(&:strip)
|
20
|
+
return false if env.include?('HTTP_CLIENT_IP') && (!ips.include? env['HTTP_CLIENT_IP'])
|
21
|
+
return false if env.include?('HTTP_X_REAL_IP') && (!ips.include? env['HTTP_X_REAL_IP'])
|
22
|
+
|
19
23
|
true
|
20
24
|
end
|
21
25
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -17,7 +19,7 @@ module Rack
|
|
17
19
|
#
|
18
20
|
# The `:allow_if` option can be set to a proc to use custom allow/deny logic.
|
19
21
|
class JsonCsrf < Base
|
20
|
-
default_options :
|
22
|
+
default_options allow_if: nil
|
21
23
|
|
22
24
|
alias react deny
|
23
25
|
|
@@ -36,8 +38,9 @@ module Rack
|
|
36
38
|
|
37
39
|
def has_vector?(request, headers)
|
38
40
|
return false if request.xhr?
|
39
|
-
return false if options[:allow_if]
|
40
|
-
return false unless headers['
|
41
|
+
return false if options[:allow_if]&.call(request.env)
|
42
|
+
return false unless headers['content-type'].to_s.split(';', 2).first =~ %r{^\s*application/json\s*$}
|
43
|
+
|
41
44
|
origin(request.env).nil? and referrer(request.env) != request.host
|
42
45
|
end
|
43
46
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -11,11 +13,11 @@ module Rack
|
|
11
13
|
# Thus <tt>GET /foo/%2e%2e%2fbar</tt> becomes <tt>GET /bar</tt>.
|
12
14
|
class PathTraversal < Base
|
13
15
|
def call(env)
|
14
|
-
path_was = env[
|
15
|
-
env[
|
16
|
+
path_was = env['PATH_INFO']
|
17
|
+
env['PATH_INFO'] = cleanup path_was if path_was && !path_was.empty?
|
16
18
|
app.call env
|
17
19
|
ensure
|
18
|
-
env[
|
20
|
+
env['PATH_INFO'] = path_was
|
19
21
|
end
|
20
22
|
|
21
23
|
def cleanup(path)
|
@@ -29,12 +31,13 @@ module Rack
|
|
29
31
|
unescaped = unescaped.gsub(backslash, slash)
|
30
32
|
|
31
33
|
unescaped.split(slash).each do |part|
|
32
|
-
next if part.empty?
|
34
|
+
next if part.empty? || (part == dot)
|
35
|
+
|
33
36
|
part == '..' ? parts.pop : parts << part
|
34
37
|
end
|
35
38
|
|
36
39
|
cleaned = slash + parts.join(slash)
|
37
|
-
cleaned << slash if parts.any?
|
40
|
+
cleaned << slash if parts.any? && unescaped =~ (%r{/\.{0,2}$})
|
38
41
|
cleaned
|
39
42
|
end
|
40
43
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -13,11 +15,11 @@ module Rack
|
|
13
15
|
# Options:
|
14
16
|
# referrer_policy:: The policy to use (default: 'strict-origin-when-cross-origin')
|
15
17
|
class ReferrerPolicy < Base
|
16
|
-
default_options :
|
18
|
+
default_options referrer_policy: 'strict-origin-when-cross-origin'
|
17
19
|
|
18
20
|
def call(env)
|
19
21
|
status, headers, body = @app.call(env)
|
20
|
-
headers['
|
22
|
+
headers['referrer-policy'] ||= options[:referrer_policy]
|
21
23
|
[status, headers, body]
|
22
24
|
end
|
23
25
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -13,23 +15,22 @@ module Rack
|
|
13
15
|
# spoofed, too, this will not prevent determined hijacking attempts.
|
14
16
|
class SessionHijacking < Base
|
15
17
|
default_reaction :drop_session
|
16
|
-
default_options :
|
17
|
-
|
18
|
+
default_options tracking_key: :tracking,
|
19
|
+
track: %w[HTTP_USER_AGENT]
|
18
20
|
|
19
21
|
def accepts?(env)
|
20
22
|
session = session env
|
21
23
|
key = options[:tracking_key]
|
22
24
|
if session.include? key
|
23
|
-
session[key].all? { |k,v| v ==
|
25
|
+
session[key].all? { |k, v| v == encode(env[k]) }
|
24
26
|
else
|
25
27
|
session[key] = {}
|
26
|
-
options[:track].each { |k| session[key][k] =
|
28
|
+
options[:track].each { |k| session[key][k] = encode(env[k]) }
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
30
|
-
def
|
31
|
-
value
|
32
|
-
options[:encrypt_tracking] ? super(value) : value
|
32
|
+
def encode(value)
|
33
|
+
value.to_s.downcase
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -18,11 +20,11 @@ module Rack
|
|
18
20
|
# preload:: Allow this domain to be included in browsers HSTS preload list. See https://hstspreload.appspot.com/
|
19
21
|
|
20
22
|
class StrictTransport < Base
|
21
|
-
default_options :
|
23
|
+
default_options max_age: 31_536_000, include_subdomains: false, preload: false
|
22
24
|
|
23
25
|
def strict_transport
|
24
26
|
@strict_transport ||= begin
|
25
|
-
strict_transport =
|
27
|
+
strict_transport = "max-age=#{options[:max_age]}"
|
26
28
|
strict_transport += '; includeSubDomains' if options[:include_subdomains]
|
27
29
|
strict_transport += '; preload' if options[:preload]
|
28
30
|
strict_transport.to_str
|
@@ -31,7 +33,7 @@ module Rack
|
|
31
33
|
|
32
34
|
def call(env)
|
33
35
|
status, headers, body = @app.call(env)
|
34
|
-
headers['
|
36
|
+
headers['strict-transport-security'] ||= strict_transport
|
35
37
|
[status, headers, body]
|
36
38
|
end
|
37
39
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
|
3
5
|
module Rack
|
@@ -12,12 +14,12 @@ module Rack
|
|
12
14
|
# Options:
|
13
15
|
# xss_mode:: How the browser should prevent the attack (default: :block)
|
14
16
|
class XSSHeader < Base
|
15
|
-
default_options :
|
17
|
+
default_options xss_mode: :block, nosniff: true
|
16
18
|
|
17
19
|
def call(env)
|
18
20
|
status, headers, body = @app.call(env)
|
19
|
-
headers['
|
20
|
-
headers['
|
21
|
+
headers['x-xss-protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers
|
22
|
+
headers['x-content-type-options'] ||= 'nosniff' if options[:nosniff]
|
21
23
|
[status, headers, body]
|
22
24
|
end
|
23
25
|
end
|
data/lib/rack/protection.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection/version'
|
2
4
|
require 'rack'
|
3
5
|
|
@@ -22,12 +24,11 @@ module Rack
|
|
22
24
|
autoload :XSSHeader, 'rack/protection/xss_header'
|
23
25
|
|
24
26
|
def self.new(app, options = {})
|
25
|
-
# does not include: RemoteReferrer, AuthenticityToken and FormToken
|
26
27
|
except = Array options[:except]
|
27
28
|
use_these = Array options[:use]
|
28
29
|
|
29
30
|
if options.fetch(:without_session, false)
|
30
|
-
except += [
|
31
|
+
except += %i[remote_token]
|
31
32
|
end
|
32
33
|
|
33
34
|
Rack::Builder.new do
|
@@ -39,6 +40,7 @@ module Rack
|
|
39
40
|
use ::Rack::Protection::FormToken, options if use_these.include? :form_token
|
40
41
|
use ::Rack::Protection::ReferrerPolicy, options if use_these.include? :referrer_policy
|
41
42
|
use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
|
43
|
+
use ::Rack::Protection::SessionHijacking, options if use_these.include? :session_hijacking
|
42
44
|
use ::Rack::Protection::StrictTransport, options if use_these.include? :strict_transport
|
43
45
|
|
44
46
|
# On by default, unless skipped
|
@@ -48,7 +50,6 @@ module Rack
|
|
48
50
|
use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
|
49
51
|
use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
|
50
52
|
use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
|
51
|
-
use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
|
52
53
|
use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
|
53
54
|
run app
|
54
55
|
end.to_app
|
data/lib/rack-protection.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require
|
1
|
+
require 'rack/protection'
|
data/rack-protection.gemspec
CHANGED
@@ -1,40 +1,44 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
version = File.read(File.expand_path('../VERSION', __dir__)).strip
|
2
4
|
|
3
5
|
Gem::Specification.new do |s|
|
4
6
|
# general infos
|
5
|
-
s.name =
|
7
|
+
s.name = 'rack-protection'
|
6
8
|
s.version = version
|
7
|
-
s.description =
|
8
|
-
s.homepage =
|
9
|
-
s.summary = s.description
|
9
|
+
s.description = 'Protect against typical web attacks, works with all Rack apps, including Rails'
|
10
|
+
s.homepage = 'https://sinatrarb.com/protection/'
|
11
|
+
s.summary = "#{s.description}."
|
10
12
|
s.license = 'MIT'
|
11
|
-
s.authors = [
|
12
|
-
s.email =
|
13
|
-
s.files = Dir[
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
s.authors = ['https://github.com/sinatra/sinatra/graphs/contributors']
|
14
|
+
s.email = 'sinatrarb@googlegroups.com'
|
15
|
+
s.files = Dir['lib/**/*.rb'] + [
|
16
|
+
'License',
|
17
|
+
'README.md',
|
18
|
+
'Rakefile',
|
19
|
+
'Gemfile',
|
20
|
+
'rack-protection.gemspec'
|
19
21
|
]
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
'source_code_uri' => 'https://github.com/sinatra/sinatra/tree/master/rack-protection',
|
24
|
-
'homepage_uri' => 'http://sinatrarb.com/protection/',
|
25
|
-
'documentation_uri' => 'https://www.rubydoc.info/gems/rack-protection'
|
26
|
-
}
|
27
|
-
else
|
28
|
-
raise <<-EOF
|
23
|
+
unless s.respond_to?(:metadata)
|
24
|
+
raise <<-WARN
|
29
25
|
RubyGems 2.0 or newer is required to protect against public gem pushes. You can update your rubygems version by running:
|
30
26
|
gem install rubygems-update
|
31
27
|
update_rubygems:
|
32
28
|
gem update --system
|
33
|
-
|
29
|
+
WARN
|
34
30
|
end
|
35
31
|
|
32
|
+
s.metadata = {
|
33
|
+
'source_code_uri' => 'https://github.com/sinatra/sinatra/tree/main/rack-protection',
|
34
|
+
'homepage_uri' => 'http://sinatrarb.com/protection/',
|
35
|
+
'documentation_uri' => 'https://www.rubydoc.info/gems/rack-protection',
|
36
|
+
'rubygems_mfa_required' => 'true'
|
37
|
+
}
|
38
|
+
|
39
|
+
s.required_ruby_version = '>= 2.7.8'
|
40
|
+
|
36
41
|
# dependencies
|
37
|
-
s.add_dependency
|
38
|
-
s.
|
39
|
-
s.add_development_dependency "rspec", "~> 3.6"
|
42
|
+
s.add_dependency 'base64', '>= 0.1.0'
|
43
|
+
s.add_dependency 'rack', '>= 3.0.0', '< 4'
|
40
44
|
end
|
metadata
CHANGED
@@ -1,59 +1,51 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-protection
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- https://github.com/sinatra/sinatra/graphs/contributors
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: base64
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.1.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 0.1.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: rack
|
28
|
+
name: rack
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
34
|
-
|
33
|
+
version: 3.0.0
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '4'
|
37
|
+
type: :runtime
|
35
38
|
prerelease: false
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
37
40
|
requirements:
|
38
41
|
- - ">="
|
39
42
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
41
|
-
-
|
42
|
-
name: rspec
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
43
|
+
version: 3.0.0
|
44
|
+
- - "<"
|
46
45
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '3.6'
|
46
|
+
version: '4'
|
55
47
|
description: Protect against typical web attacks, works with all Rack apps, including
|
56
|
-
Rails
|
48
|
+
Rails
|
57
49
|
email: sinatrarb@googlegroups.com
|
58
50
|
executables: []
|
59
51
|
extensions: []
|
@@ -83,15 +75,17 @@ files:
|
|
83
75
|
- lib/rack/protection/strict_transport.rb
|
84
76
|
- lib/rack/protection/version.rb
|
85
77
|
- lib/rack/protection/xss_header.rb
|
78
|
+
- lib/rack_protection.rb
|
86
79
|
- rack-protection.gemspec
|
87
|
-
homepage:
|
80
|
+
homepage: https://sinatrarb.com/protection/
|
88
81
|
licenses:
|
89
82
|
- MIT
|
90
83
|
metadata:
|
91
|
-
source_code_uri: https://github.com/sinatra/sinatra/tree/
|
84
|
+
source_code_uri: https://github.com/sinatra/sinatra/tree/main/rack-protection
|
92
85
|
homepage_uri: http://sinatrarb.com/protection/
|
93
86
|
documentation_uri: https://www.rubydoc.info/gems/rack-protection
|
94
|
-
|
87
|
+
rubygems_mfa_required: 'true'
|
88
|
+
post_install_message:
|
95
89
|
rdoc_options: []
|
96
90
|
require_paths:
|
97
91
|
- lib
|
@@ -99,15 +93,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
99
93
|
requirements:
|
100
94
|
- - ">="
|
101
95
|
- !ruby/object:Gem::Version
|
102
|
-
version:
|
96
|
+
version: 2.7.8
|
103
97
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
98
|
requirements:
|
105
99
|
- - ">="
|
106
100
|
- !ruby/object:Gem::Version
|
107
101
|
version: '0'
|
108
102
|
requirements: []
|
109
|
-
rubygems_version: 3.
|
110
|
-
signing_key:
|
103
|
+
rubygems_version: 3.5.3
|
104
|
+
signing_key:
|
111
105
|
specification_version: 4
|
112
106
|
summary: Protect against typical web attacks, works with all Rack apps, including
|
113
107
|
Rails.
|