homura-runtime 0.3.3 → 0.3.4
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/CHANGELOG.md +7 -0
- data/lib/homura/runtime/version.rb +1 -1
- data/vendor/rack/auth/abstract/handler.rb +41 -0
- data/vendor/rack/auth/abstract/request.rb +51 -0
- data/vendor/rack/auth/basic.rb +58 -0
- data/vendor/rack/bad_request.rb +8 -0
- data/vendor/rack/body_proxy.rb +63 -0
- data/vendor/rack/builder.rb +315 -0
- data/vendor/rack/cascade.rb +67 -0
- data/vendor/rack/common_logger.rb +94 -0
- data/vendor/rack/conditional_get.rb +87 -0
- data/vendor/rack/config.rb +22 -0
- data/vendor/rack/constants.rb +68 -0
- data/vendor/rack/content_length.rb +34 -0
- data/vendor/rack/content_type.rb +33 -0
- data/vendor/rack/deflater.rb +159 -0
- data/vendor/rack/directory.rb +210 -0
- data/vendor/rack/etag.rb +71 -0
- data/vendor/rack/events.rb +172 -0
- data/vendor/rack/files.rb +224 -0
- data/vendor/rack/head.rb +25 -0
- data/vendor/rack/headers.rb +238 -0
- data/vendor/rack/lint.rb +1000 -0
- data/vendor/rack/lock.rb +29 -0
- data/vendor/rack/media_type.rb +42 -0
- data/vendor/rack/method_override.rb +56 -0
- data/vendor/rack/mime.rb +694 -0
- data/vendor/rack/mock.rb +3 -0
- data/vendor/rack/mock_request.rb +161 -0
- data/vendor/rack/mock_response.rb +147 -0
- data/vendor/rack/multipart/generator.rb +99 -0
- data/vendor/rack/multipart/parser.rb +586 -0
- data/vendor/rack/multipart/uploaded_file.rb +82 -0
- data/vendor/rack/multipart.rb +77 -0
- data/vendor/rack/null_logger.rb +48 -0
- data/vendor/rack/protection/authenticity_token.rb +256 -0
- data/vendor/rack/protection/base.rb +140 -0
- data/vendor/rack/protection/content_security_policy.rb +80 -0
- data/vendor/rack/protection/cookie_tossing.rb +77 -0
- data/vendor/rack/protection/escaped_params.rb +93 -0
- data/vendor/rack/protection/form_token.rb +25 -0
- data/vendor/rack/protection/frame_options.rb +39 -0
- data/vendor/rack/protection/http_origin.rb +43 -0
- data/vendor/rack/protection/ip_spoofing.rb +27 -0
- data/vendor/rack/protection/json_csrf.rb +60 -0
- data/vendor/rack/protection/path_traversal.rb +45 -0
- data/vendor/rack/protection/referrer_policy.rb +27 -0
- data/vendor/rack/protection/remote_referrer.rb +22 -0
- data/vendor/rack/protection/remote_token.rb +24 -0
- data/vendor/rack/protection/session_hijacking.rb +37 -0
- data/vendor/rack/protection/strict_transport.rb +41 -0
- data/vendor/rack/protection/version.rb +7 -0
- data/vendor/rack/protection/xss_header.rb +27 -0
- data/vendor/rack/protection.rb +58 -0
- data/vendor/rack/query_parser.rb +261 -0
- data/vendor/rack/recursive.rb +66 -0
- data/vendor/rack/reloader.rb +112 -0
- data/vendor/rack/request.rb +818 -0
- data/vendor/rack/response.rb +403 -0
- data/vendor/rack/rewindable_input.rb +116 -0
- data/vendor/rack/runtime.rb +35 -0
- data/vendor/rack/sendfile.rb +197 -0
- data/vendor/rack/session/abstract/id.rb +533 -0
- data/vendor/rack/session/constants.rb +13 -0
- data/vendor/rack/session/cookie.rb +292 -0
- data/vendor/rack/session/encryptor.rb +415 -0
- data/vendor/rack/session/pool.rb +76 -0
- data/vendor/rack/session/version.rb +10 -0
- data/vendor/rack/session.rb +12 -0
- data/vendor/rack/show_exceptions.rb +433 -0
- data/vendor/rack/show_status.rb +121 -0
- data/vendor/rack/static.rb +188 -0
- data/vendor/rack/tempfile_reaper.rb +44 -0
- data/vendor/rack/urlmap.rb +99 -0
- data/vendor/rack/utils.rb +631 -0
- data/vendor/rack/version.rb +17 -0
- data/vendor/rack.rb +66 -0
- metadata +76 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'constants'
|
|
4
|
+
require_relative 'utils'
|
|
5
|
+
|
|
6
|
+
require_relative 'multipart/parser'
|
|
7
|
+
require_relative 'multipart/generator'
|
|
8
|
+
|
|
9
|
+
require_relative 'bad_request'
|
|
10
|
+
|
|
11
|
+
module Rack
|
|
12
|
+
# A multipart form data parser, adapted from IOWA.
|
|
13
|
+
#
|
|
14
|
+
# Usually, Rack::Request#POST takes care of calling this.
|
|
15
|
+
module Multipart
|
|
16
|
+
MULTIPART_BOUNDARY = "AaB03x"
|
|
17
|
+
|
|
18
|
+
class MissingInputError < StandardError
|
|
19
|
+
include BadRequest
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Accumulator for multipart form data, conforming to the QueryParser API.
|
|
23
|
+
# In future, the Parser could return the pair list directly, but that would
|
|
24
|
+
# change its API.
|
|
25
|
+
class ParamList # :nodoc:
|
|
26
|
+
def self.make_params
|
|
27
|
+
new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.normalize_params(params, key, value)
|
|
31
|
+
params << [key, value]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize
|
|
35
|
+
@pairs = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def <<(pair)
|
|
39
|
+
@pairs << pair
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_params_hash
|
|
43
|
+
@pairs
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
def parse_multipart(env, params = Rack::Utils.default_query_parser)
|
|
49
|
+
unless io = env[RACK_INPUT]
|
|
50
|
+
raise MissingInputError, "Missing input stream!"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if content_length = env['CONTENT_LENGTH']
|
|
54
|
+
content_length = content_length.to_i
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
content_type = env['CONTENT_TYPE']
|
|
58
|
+
|
|
59
|
+
tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
|
|
60
|
+
bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
|
|
61
|
+
|
|
62
|
+
info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
|
|
63
|
+
env[RACK_TEMPFILES] = info.tmp_files
|
|
64
|
+
|
|
65
|
+
return info.params
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def extract_multipart(request, params = Rack::Utils.default_query_parser)
|
|
69
|
+
parse_multipart(request.env)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_multipart(params, first = true)
|
|
73
|
+
Generator.new(params, first).dump
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'constants'
|
|
4
|
+
|
|
5
|
+
module Rack
|
|
6
|
+
class NullLogger
|
|
7
|
+
def initialize(app)
|
|
8
|
+
@app = app
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(env)
|
|
12
|
+
env[RACK_LOGGER] = self
|
|
13
|
+
@app.call(env)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def info(progname = nil, &block); end
|
|
17
|
+
def debug(progname = nil, &block); end
|
|
18
|
+
def warn(progname = nil, &block); end
|
|
19
|
+
def error(progname = nil, &block); end
|
|
20
|
+
def fatal(progname = nil, &block); end
|
|
21
|
+
def unknown(progname = nil, &block); end
|
|
22
|
+
def info? ; end
|
|
23
|
+
def debug? ; end
|
|
24
|
+
def warn? ; end
|
|
25
|
+
def error? ; end
|
|
26
|
+
def fatal? ; end
|
|
27
|
+
def debug! ; end
|
|
28
|
+
def error! ; end
|
|
29
|
+
def fatal! ; end
|
|
30
|
+
def info! ; end
|
|
31
|
+
def warn! ; end
|
|
32
|
+
def level ; end
|
|
33
|
+
def progname ; end
|
|
34
|
+
def datetime_format ; end
|
|
35
|
+
def formatter ; end
|
|
36
|
+
def sev_threshold ; end
|
|
37
|
+
def level=(level); end
|
|
38
|
+
def progname=(progname); end
|
|
39
|
+
def datetime_format=(datetime_format); end
|
|
40
|
+
def formatter=(formatter); end
|
|
41
|
+
def sev_threshold=(sev_threshold); end
|
|
42
|
+
def close ; end
|
|
43
|
+
def add(severity, message = nil, progname = nil, &block); end
|
|
44
|
+
def log(severity, message = nil, progname = nil, &block); end
|
|
45
|
+
def <<(msg); end
|
|
46
|
+
def reopen(logdev = nil); end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack/protection'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
# require 'openssl'
|
|
6
|
+
require 'base64'
|
|
7
|
+
|
|
8
|
+
module Rack
|
|
9
|
+
module Protection
|
|
10
|
+
##
|
|
11
|
+
# Prevented attack:: CSRF
|
|
12
|
+
# Supported browsers:: all
|
|
13
|
+
# More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
|
|
14
|
+
#
|
|
15
|
+
# This middleware only accepts requests other than <tt>GET</tt>,
|
|
16
|
+
# <tt>HEAD</tt>, <tt>OPTIONS</tt>, <tt>TRACE</tt> if their given access
|
|
17
|
+
# token matches the token included in the session.
|
|
18
|
+
#
|
|
19
|
+
# It checks the <tt>X-CSRF-Token</tt> header and the <tt>POST</tt> form
|
|
20
|
+
# data.
|
|
21
|
+
#
|
|
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")
|
|
26
|
+
#
|
|
27
|
+
# == Options
|
|
28
|
+
#
|
|
29
|
+
# [<tt>:authenticity_param</tt>] the name of the param that should contain
|
|
30
|
+
# the token on a request. Default value:
|
|
31
|
+
# <tt>"authenticity_token"</tt>
|
|
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
|
+
#
|
|
40
|
+
# == Example: Forms application
|
|
41
|
+
#
|
|
42
|
+
# To show what the AuthenticityToken does, this section includes a sample
|
|
43
|
+
# program which shows two forms. One with, and one without a CSRF token
|
|
44
|
+
# The one without CSRF token field will get a 403 Forbidden response.
|
|
45
|
+
#
|
|
46
|
+
# Install the gem, then run the program:
|
|
47
|
+
#
|
|
48
|
+
# gem install 'rack-protection'
|
|
49
|
+
# ruby server.rb
|
|
50
|
+
#
|
|
51
|
+
# Here is <tt>server.rb</tt>:
|
|
52
|
+
#
|
|
53
|
+
# require 'rack/protection'
|
|
54
|
+
# require 'rack/session'
|
|
55
|
+
#
|
|
56
|
+
# app = Rack::Builder.app do
|
|
57
|
+
# use Rack::Session::Cookie, secret: 'secret'
|
|
58
|
+
# use Rack::Protection::AuthenticityToken
|
|
59
|
+
#
|
|
60
|
+
# run -> (env) do
|
|
61
|
+
# [200, {}, [
|
|
62
|
+
# <<~EOS
|
|
63
|
+
# <!DOCTYPE html>
|
|
64
|
+
# <html lang="en">
|
|
65
|
+
# <head>
|
|
66
|
+
# <meta charset="UTF-8" />
|
|
67
|
+
# <title>rack-protection minimal example</title>
|
|
68
|
+
# </head>
|
|
69
|
+
# <body>
|
|
70
|
+
# <h1>Without Authenticity Token</h1>
|
|
71
|
+
# <p>This takes you to <tt>Forbidden</tt></p>
|
|
72
|
+
# <form action="" method="post">
|
|
73
|
+
# <input type="text" name="foo" />
|
|
74
|
+
# <input type="submit" />
|
|
75
|
+
# </form>
|
|
76
|
+
#
|
|
77
|
+
# <h1>With Authenticity Token</h1>
|
|
78
|
+
# <p>This successfully takes you to back to this form.</p>
|
|
79
|
+
# <form action="" method="post">
|
|
80
|
+
# <input type="hidden" name="authenticity_token" value="#{Rack::Protection::AuthenticityToken.token(env['rack.session'])}" />
|
|
81
|
+
# <input type="text" name="foo" />
|
|
82
|
+
# <input type="submit" />
|
|
83
|
+
# </form>
|
|
84
|
+
# </body>
|
|
85
|
+
# </html>
|
|
86
|
+
# EOS
|
|
87
|
+
# ]]
|
|
88
|
+
# end
|
|
89
|
+
# end
|
|
90
|
+
#
|
|
91
|
+
# Rack::Handler::WEBrick.run app
|
|
92
|
+
#
|
|
93
|
+
# == Example: Customize which POST parameter holds the token
|
|
94
|
+
#
|
|
95
|
+
# To customize the authenticity parameter for form data, use the
|
|
96
|
+
# <tt>:authenticity_param</tt> option:
|
|
97
|
+
# use Rack::Protection::AuthenticityToken, authenticity_param: 'your_token_param_name'
|
|
98
|
+
class AuthenticityToken < Base
|
|
99
|
+
TOKEN_LENGTH = 32
|
|
100
|
+
|
|
101
|
+
default_options authenticity_param: 'authenticity_token',
|
|
102
|
+
key: :csrf,
|
|
103
|
+
allow_if: nil
|
|
104
|
+
|
|
105
|
+
def self.token(session, path: nil, method: :post)
|
|
106
|
+
new(nil).mask_authenticity_token(session, path: path, method: method)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.random_token
|
|
110
|
+
SecureRandom.urlsafe_base64(TOKEN_LENGTH, padding: false)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def accepts?(env)
|
|
114
|
+
session = session(env)
|
|
115
|
+
set_token(session)
|
|
116
|
+
|
|
117
|
+
safe?(env) ||
|
|
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
|
|
123
|
+
end
|
|
124
|
+
|
|
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
|
+
|
|
134
|
+
mask_token(token)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
GLOBAL_TOKEN_IDENTIFIER = '!real_csrf_token'
|
|
138
|
+
private_constant :GLOBAL_TOKEN_IDENTIFIER
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def set_token(session)
|
|
143
|
+
session[options[:key]] ||= self.class.random_token
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Checks the client's masked token to see if it matches the
|
|
147
|
+
# session token.
|
|
148
|
+
def valid_token?(env, token)
|
|
149
|
+
return false if token.nil? || !token.is_a?(String) || token.empty?
|
|
150
|
+
|
|
151
|
+
session = session(env)
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
token = decode_token(token)
|
|
155
|
+
rescue ArgumentError # encoded_masked_token is invalid Base64
|
|
156
|
+
return false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# See if it's actually a masked token or not. We should be able
|
|
160
|
+
# to handle any unmasked tokens that we've issued without error.
|
|
161
|
+
|
|
162
|
+
if unmasked_token?(token)
|
|
163
|
+
compare_with_real_token(token, session)
|
|
164
|
+
elsif masked_token?(token)
|
|
165
|
+
token = unmask_token(token)
|
|
166
|
+
|
|
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))
|
|
170
|
+
else
|
|
171
|
+
false # Token is malformed
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Creates a masked version of the authenticity token that varies
|
|
176
|
+
# on each request. The masking is used to mitigate SSL attacks
|
|
177
|
+
# like BREACH.
|
|
178
|
+
def mask_token(token)
|
|
179
|
+
one_time_pad = SecureRandom.random_bytes(token.length)
|
|
180
|
+
encrypted_token = xor_byte_strings(one_time_pad, token)
|
|
181
|
+
masked_token = one_time_pad + encrypted_token
|
|
182
|
+
encode_token(masked_token)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Essentially the inverse of +mask_token+.
|
|
186
|
+
def unmask_token(masked_token)
|
|
187
|
+
# Split the token into the one-time pad and the encrypted
|
|
188
|
+
# value and decrypt it
|
|
189
|
+
token_length = masked_token.length / 2
|
|
190
|
+
one_time_pad = masked_token[0...token_length]
|
|
191
|
+
encrypted_token = masked_token[token_length..]
|
|
192
|
+
xor_byte_strings(one_time_pad, encrypted_token)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def unmasked_token?(token)
|
|
196
|
+
token.length == TOKEN_LENGTH
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def masked_token?(token)
|
|
200
|
+
token.length == TOKEN_LENGTH * 2
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def compare_with_real_token(token, session)
|
|
204
|
+
secure_compare(token, real_token(session))
|
|
205
|
+
end
|
|
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
|
+
|
|
216
|
+
def real_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}")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def encode_token(token)
|
|
229
|
+
Base64.urlsafe_encode64(token)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def decode_token(token)
|
|
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
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def xor_byte_strings(s1, s2)
|
|
245
|
+
s2 = s2.dup
|
|
246
|
+
size = s1.bytesize
|
|
247
|
+
i = 0
|
|
248
|
+
while i < size
|
|
249
|
+
s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
|
|
250
|
+
i += 1
|
|
251
|
+
end
|
|
252
|
+
s2
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack/protection'
|
|
4
|
+
require 'rack/utils'
|
|
5
|
+
# require 'digest'
|
|
6
|
+
require 'logger'
|
|
7
|
+
require 'uri'
|
|
8
|
+
|
|
9
|
+
module Rack
|
|
10
|
+
module Protection
|
|
11
|
+
class Base
|
|
12
|
+
DEFAULT_OPTIONS = {
|
|
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]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
attr_reader :app, :options
|
|
22
|
+
|
|
23
|
+
def self.default_options(options)
|
|
24
|
+
define_method(:default_options) { super().merge(options) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.default_reaction(reaction)
|
|
28
|
+
alias_method(:default_reaction, reaction)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def default_options
|
|
32
|
+
DEFAULT_OPTIONS
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(app, options = {})
|
|
36
|
+
@app = app
|
|
37
|
+
@options = default_options.merge(options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def safe?(env)
|
|
41
|
+
%w[GET HEAD OPTIONS TRACE].include? env['REQUEST_METHOD']
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def accepts?(env)
|
|
45
|
+
raise NotImplementedError, "#{self.class} implementation pending"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call(env)
|
|
49
|
+
unless accepts? env
|
|
50
|
+
instrument env
|
|
51
|
+
result = react env
|
|
52
|
+
end
|
|
53
|
+
result or app.call(env)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def react(env)
|
|
57
|
+
result = send(options[:reaction], env)
|
|
58
|
+
result if (Array === result) && (result.size == 3)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def warn(env, message)
|
|
62
|
+
return unless options[:logging]
|
|
63
|
+
|
|
64
|
+
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
|
|
65
|
+
l.warn(message)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def instrument(env)
|
|
69
|
+
return unless (i = options[:instrumenter])
|
|
70
|
+
|
|
71
|
+
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
|
|
72
|
+
i.instrument('rack.protection', env)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def deny(env)
|
|
76
|
+
warn env, "attack prevented by #{self.class}"
|
|
77
|
+
[options[:status], { 'content-type' => 'text/plain' }, [options[:message]]]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def report(env)
|
|
81
|
+
warn env, "attack reported by #{self.class}"
|
|
82
|
+
env[options[:report_key]] = true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def session?(env)
|
|
86
|
+
env.include? options[:session_key]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def session(env)
|
|
90
|
+
return env[options[:session_key]] if session? env
|
|
91
|
+
|
|
92
|
+
raise "you need to set up a session middleware *before* #{self.class}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def drop_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}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def referrer(env)
|
|
106
|
+
ref = env['HTTP_REFERER'].to_s
|
|
107
|
+
return if !options[:allow_empty_referrer] && ref.empty?
|
|
108
|
+
|
|
109
|
+
URI.parse(ref).host || Request.new(env).host
|
|
110
|
+
rescue URI::InvalidURIError
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def origin(env)
|
|
114
|
+
env['HTTP_ORIGIN'] || env['HTTP_X_ORIGIN']
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def random_string(secure = defined? SecureRandom)
|
|
118
|
+
secure ? SecureRandom.hex(16) : '%032x' % rand((2**128) - 1)
|
|
119
|
+
rescue NotImplementedError
|
|
120
|
+
random_string false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def encrypt(value)
|
|
124
|
+
options[:encryptor].hexdigest value.to_s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def secure_compare(a, b)
|
|
128
|
+
Rack::Utils.secure_compare(a.to_s, b.to_s)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
alias default_reaction deny
|
|
132
|
+
|
|
133
|
+
def html?(headers)
|
|
134
|
+
return false unless (header = headers.detect { |k, _v| k.downcase == 'content-type' })
|
|
135
|
+
|
|
136
|
+
options[:html_types].include? header.last[%r{^\w+/\w+}]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack/protection'
|
|
4
|
+
|
|
5
|
+
module Rack
|
|
6
|
+
module Protection
|
|
7
|
+
##
|
|
8
|
+
# Prevented attack:: XSS and others
|
|
9
|
+
# Supported browsers:: Firefox 23+, Safari 7+, Chrome 25+, Opera 15+
|
|
10
|
+
#
|
|
11
|
+
# Description:: Content Security Policy, a mechanism web applications
|
|
12
|
+
# can use to mitigate a broad class of content injection
|
|
13
|
+
# vulnerabilities, such as cross-site scripting (XSS).
|
|
14
|
+
# Content Security Policy is a declarative policy that lets
|
|
15
|
+
# the authors (or server administrators) of a web application
|
|
16
|
+
# inform the client about the sources from which the
|
|
17
|
+
# application expects to load resources.
|
|
18
|
+
#
|
|
19
|
+
# More info:: W3C CSP Level 1 : https://www.w3.org/TR/CSP1/ (deprecated)
|
|
20
|
+
# W3C CSP Level 2 : https://www.w3.org/TR/CSP2/ (current)
|
|
21
|
+
# W3C CSP Level 3 : https://www.w3.org/TR/CSP3/ (draft)
|
|
22
|
+
# https://developer.mozilla.org/en-US/docs/Web/Security/CSP
|
|
23
|
+
# http://caniuse.com/#search=ContentSecurityPolicy
|
|
24
|
+
# http://content-security-policy.com/
|
|
25
|
+
# https://securityheaders.io
|
|
26
|
+
# https://scotthelme.co.uk/csp-cheat-sheet/
|
|
27
|
+
# http://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
|
28
|
+
#
|
|
29
|
+
# Sets the 'content-security-policy[-report-only]' header.
|
|
30
|
+
#
|
|
31
|
+
# Options: ContentSecurityPolicy configuration is a complex topic with
|
|
32
|
+
# several levels of support that has evolved over time.
|
|
33
|
+
# See the W3C documentation and the links in the more info
|
|
34
|
+
# section for CSP usage examples and best practices. The
|
|
35
|
+
# CSP3 directives in the 'NO_ARG_DIRECTIVES' constant need to be
|
|
36
|
+
# presented in the options hash with a boolean 'true' in order
|
|
37
|
+
# to be used in a policy.
|
|
38
|
+
#
|
|
39
|
+
class ContentSecurityPolicy < Base
|
|
40
|
+
default_options default_src: "'self'", report_only: false
|
|
41
|
+
|
|
42
|
+
DIRECTIVES = %i[base_uri child_src connect_src default_src
|
|
43
|
+
font_src form_action frame_ancestors frame_src
|
|
44
|
+
img_src manifest_src media_src object_src
|
|
45
|
+
plugin_types referrer reflected_xss report_to
|
|
46
|
+
report_uri require_sri_for sandbox script_src
|
|
47
|
+
style_src worker_src webrtc_src navigate_to
|
|
48
|
+
prefetch_src].freeze
|
|
49
|
+
|
|
50
|
+
NO_ARG_DIRECTIVES = %i[block_all_mixed_content disown_opener
|
|
51
|
+
upgrade_insecure_requests].freeze
|
|
52
|
+
|
|
53
|
+
def csp_policy
|
|
54
|
+
directives = []
|
|
55
|
+
|
|
56
|
+
DIRECTIVES.each do |d|
|
|
57
|
+
if options.key?(d)
|
|
58
|
+
directives << "#{d.to_s.sub(/_/, '-')} #{options[d]}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Set these key values to boolean 'true' to include in policy
|
|
63
|
+
NO_ARG_DIRECTIVES.each do |d|
|
|
64
|
+
if options.key?(d) && options[d].is_a?(TrueClass)
|
|
65
|
+
directives << d.to_s.tr('_', '-')
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
directives.compact.sort.join('; ')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def call(env)
|
|
73
|
+
status, headers, body = @app.call(env)
|
|
74
|
+
header = options[:report_only] ? 'content-security-policy-report-only' : 'content-security-policy'
|
|
75
|
+
headers[header] ||= csp_policy if html? headers
|
|
76
|
+
[status, headers, body]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack/protection'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module Rack
|
|
7
|
+
module Protection
|
|
8
|
+
##
|
|
9
|
+
# Prevented attack:: Cookie Tossing
|
|
10
|
+
# Supported browsers:: all
|
|
11
|
+
# More infos:: https://github.com/blog/1466-yummy-cookies-across-domains
|
|
12
|
+
#
|
|
13
|
+
# Does not accept HTTP requests if the HTTP_COOKIE header contains more than one
|
|
14
|
+
# session cookie. This does not protect against a cookie overflow attack.
|
|
15
|
+
#
|
|
16
|
+
# Options:
|
|
17
|
+
#
|
|
18
|
+
# session_key:: The name of the session cookie (default: 'rack.session')
|
|
19
|
+
class CookieTossing < Base
|
|
20
|
+
default_reaction :deny
|
|
21
|
+
|
|
22
|
+
def call(env)
|
|
23
|
+
status, headers, body = super
|
|
24
|
+
response = Rack::Response.new(body, status, headers)
|
|
25
|
+
request = Rack::Request.new(env)
|
|
26
|
+
remove_bad_cookies(request, response)
|
|
27
|
+
response.finish
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def accepts?(env)
|
|
31
|
+
cookie_header = env['HTTP_COOKIE']
|
|
32
|
+
cookies = Rack::Utils.parse_query(cookie_header, ';,') { |s| s }
|
|
33
|
+
cookies.each do |k, v|
|
|
34
|
+
if (k == session_key && Array(v).size > 1) ||
|
|
35
|
+
(k != session_key && Rack::Utils.unescape(k) == session_key)
|
|
36
|
+
bad_cookies << k
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
bad_cookies.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def remove_bad_cookies(request, response)
|
|
43
|
+
return if bad_cookies.empty?
|
|
44
|
+
|
|
45
|
+
paths = cookie_paths(request.path)
|
|
46
|
+
bad_cookies.each do |name|
|
|
47
|
+
paths.each { |path| response.set_cookie name, empty_cookie(request.host, path) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def redirect(env)
|
|
52
|
+
request = Request.new(env)
|
|
53
|
+
warn env, "attack prevented by #{self.class}"
|
|
54
|
+
[302, { 'content-type' => 'text/html', 'location' => request.path }, []]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def bad_cookies
|
|
58
|
+
@bad_cookies ||= []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def cookie_paths(path)
|
|
62
|
+
path = '/' if path.to_s.empty?
|
|
63
|
+
paths = []
|
|
64
|
+
Pathname.new(path).descend { |p| paths << p.to_s }
|
|
65
|
+
paths
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def empty_cookie(host, path)
|
|
69
|
+
{ value: '', domain: host, path: path, expires: Time.at(0) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def session_key
|
|
73
|
+
@session_key ||= options[:session_key]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|