rack-protection 2.1.0 → 3.2.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 -5
- data/README.md +2 -0
- data/Rakefile +24 -22
- data/lib/rack/protection/authenticity_token.rb +76 -24
- data/lib/rack/protection/base.rb +30 -16
- data/lib/rack/protection/content_security_policy.rb +6 -5
- data/lib/rack/protection/cookie_tossing.rb +7 -5
- data/lib/rack/protection/encrypted_cookie.rb +273 -0
- data/lib/rack/protection/encryptor.rb +62 -0
- data/lib/rack/protection/escaped_params.rb +14 -10
- data/lib/rack/protection/form_token.rb +3 -1
- data/lib/rack/protection/frame_options.rb +3 -1
- data/lib/rack/protection/http_origin.rb +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 +3 -1
- data/lib/rack/protection/remote_referrer.rb +2 -0
- data/lib/rack/protection/remote_token.rb +2 -0
- data/lib/rack/protection/session_hijacking.rb +8 -7
- data/lib/rack/protection/strict_transport.rb +4 -2
- data/lib/rack/protection/version.rb +3 -1
- data/lib/rack/protection/xss_header.rb +3 -1
- data/lib/rack/protection.rb +5 -1
- data/lib/rack-protection.rb +1 -1
- data/lib/rack_protection.rb +3 -0
- data/rack-protection.gemspec +29 -25
- metadata +23 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7640a15f8659807abd53474e7ce538a42e476e4bd99dc745f3b9b8c16161c008
|
4
|
+
data.tar.gz: '05468ec6c8113d3afce2df62221e4c866616999700c30ba3ef94a2705b11138b'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eeaff5e584a8ee3be6c80dc92c67fcc95bdbb97b084509ed90ca9ad524598fba63690cfa372586edd27940cd609fa44210637e9c95fbf1191e1a5cc297f222ac
|
7
|
+
data.tar.gz: 26e2160d65b6015c7aaa52266b7241d15f645eb259d1371b864b3e2b6a3b1fbef841e62304bc8e39a83fab1a52ddb0c3455a51385a9a451c833cbed91b75d00a
|
data/Gemfile
CHANGED
@@ -1,13 +1,16 @@
|
|
1
|
-
|
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?
|
8
|
-
rack_version = {:
|
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
|
-
#
|
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
|
@@ -84,42 +97,57 @@ module Rack
|
|
84
97
|
class AuthenticityToken < Base
|
85
98
|
TOKEN_LENGTH = 32
|
86
99
|
|
87
|
-
default_options :
|
88
|
-
:
|
100
|
+
default_options authenticity_param: 'authenticity_token',
|
101
|
+
key: :csrf,
|
102
|
+
allow_if: nil
|
89
103
|
|
90
|
-
def self.token(session)
|
91
|
-
|
104
|
+
def self.token(session, path: nil, method: :post)
|
105
|
+
new(nil).mask_authenticity_token(session, path: path, method: method)
|
92
106
|
end
|
93
107
|
|
94
108
|
def self.random_token
|
95
|
-
SecureRandom.
|
109
|
+
SecureRandom.urlsafe_base64(TOKEN_LENGTH, padding: false)
|
96
110
|
end
|
97
111
|
|
98
112
|
def accepts?(env)
|
99
|
-
session = session
|
113
|
+
session = session(env)
|
100
114
|
set_token(session)
|
101
115
|
|
102
116
|
safe?(env) ||
|
103
|
-
valid_token?(
|
104
|
-
valid_token?(
|
105
|
-
|
117
|
+
valid_token?(env, env['HTTP_X_CSRF_TOKEN']) ||
|
118
|
+
valid_token?(env, Request.new(env).params[options[:authenticity_param]]) ||
|
119
|
+
options[:allow_if]&.call(env)
|
120
|
+
rescue StandardError
|
121
|
+
false
|
106
122
|
end
|
107
123
|
|
108
|
-
def mask_authenticity_token(session)
|
109
|
-
|
124
|
+
def mask_authenticity_token(session, path: nil, method: :post)
|
125
|
+
set_token(session)
|
126
|
+
|
127
|
+
token = if path && method
|
128
|
+
per_form_token(session, path, method)
|
129
|
+
else
|
130
|
+
global_token(session)
|
131
|
+
end
|
132
|
+
|
110
133
|
mask_token(token)
|
111
134
|
end
|
112
135
|
|
136
|
+
GLOBAL_TOKEN_IDENTIFIER = '!real_csrf_token'
|
137
|
+
private_constant :GLOBAL_TOKEN_IDENTIFIER
|
138
|
+
|
113
139
|
private
|
114
140
|
|
115
141
|
def set_token(session)
|
116
|
-
session[:
|
142
|
+
session[options[:key]] ||= self.class.random_token
|
117
143
|
end
|
118
144
|
|
119
145
|
# Checks the client's masked token to see if it matches the
|
120
146
|
# session token.
|
121
|
-
def valid_token?(
|
122
|
-
return false if token.nil? || token.empty?
|
147
|
+
def valid_token?(env, token)
|
148
|
+
return false if token.nil? || !token.is_a?(String) || token.empty?
|
149
|
+
|
150
|
+
session = session(env)
|
123
151
|
|
124
152
|
begin
|
125
153
|
token = decode_token(token)
|
@@ -131,13 +159,13 @@ module Rack
|
|
131
159
|
# to handle any unmasked tokens that we've issued without error.
|
132
160
|
|
133
161
|
if unmasked_token?(token)
|
134
|
-
compare_with_real_token
|
135
|
-
|
162
|
+
compare_with_real_token(token, session)
|
136
163
|
elsif masked_token?(token)
|
137
164
|
token = unmask_token(token)
|
138
165
|
|
139
|
-
|
140
|
-
|
166
|
+
compare_with_global_token(token, session) ||
|
167
|
+
compare_with_real_token(token, session) ||
|
168
|
+
compare_with_per_form_token(token, session, Request.new(env))
|
141
169
|
else
|
142
170
|
false # Token is malformed
|
143
171
|
end
|
@@ -147,7 +175,6 @@ module Rack
|
|
147
175
|
# on each request. The masking is used to mitigate SSL attacks
|
148
176
|
# like BREACH.
|
149
177
|
def mask_token(token)
|
150
|
-
token = decode_token(token)
|
151
178
|
one_time_pad = SecureRandom.random_bytes(token.length)
|
152
179
|
encrypted_token = xor_byte_strings(one_time_pad, token)
|
153
180
|
masked_token = one_time_pad + encrypted_token
|
@@ -160,7 +187,7 @@ module Rack
|
|
160
187
|
# value and decrypt it
|
161
188
|
token_length = masked_token.length / 2
|
162
189
|
one_time_pad = masked_token[0...token_length]
|
163
|
-
encrypted_token = masked_token[token_length
|
190
|
+
encrypted_token = masked_token[token_length..]
|
164
191
|
xor_byte_strings(one_time_pad, encrypted_token)
|
165
192
|
end
|
166
193
|
|
@@ -176,16 +203,41 @@ module Rack
|
|
176
203
|
secure_compare(token, real_token(session))
|
177
204
|
end
|
178
205
|
|
206
|
+
def compare_with_global_token(token, session)
|
207
|
+
secure_compare(token, global_token(session))
|
208
|
+
end
|
209
|
+
|
210
|
+
def compare_with_per_form_token(token, session, request)
|
211
|
+
secure_compare(token,
|
212
|
+
per_form_token(session, request.path.chomp('/'), request.request_method))
|
213
|
+
end
|
214
|
+
|
179
215
|
def real_token(session)
|
180
|
-
decode_token(session[:
|
216
|
+
decode_token(session[options[:key]])
|
217
|
+
end
|
218
|
+
|
219
|
+
def global_token(session)
|
220
|
+
token_hmac(session, GLOBAL_TOKEN_IDENTIFIER)
|
221
|
+
end
|
222
|
+
|
223
|
+
def per_form_token(session, path, method)
|
224
|
+
token_hmac(session, "#{path}##{method.downcase}")
|
181
225
|
end
|
182
226
|
|
183
227
|
def encode_token(token)
|
184
|
-
Base64.
|
228
|
+
Base64.urlsafe_encode64(token)
|
185
229
|
end
|
186
230
|
|
187
231
|
def decode_token(token)
|
188
|
-
Base64.
|
232
|
+
Base64.urlsafe_decode64(token)
|
233
|
+
end
|
234
|
+
|
235
|
+
def token_hmac(session, identifier)
|
236
|
+
OpenSSL::HMAC.digest(
|
237
|
+
OpenSSL::Digest.new('SHA256'),
|
238
|
+
real_token(session),
|
239
|
+
identifier
|
240
|
+
)
|
189
241
|
end
|
190
242
|
|
191
243
|
def xor_byte_strings(s1, s2)
|
data/lib/rack/protection/base.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
require 'rack/utils'
|
3
5
|
require 'digest'
|
@@ -8,12 +10,12 @@ module Rack
|
|
8
10
|
module Protection
|
9
11
|
class Base
|
10
12
|
DEFAULT_OPTIONS = {
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
13
|
+
reaction: :default_reaction, logging: true,
|
14
|
+
message: 'Forbidden', encryptor: Digest::SHA1,
|
15
|
+
session_key: 'rack.session', status: 403,
|
16
|
+
allow_empty_referrer: true,
|
17
|
+
report_key: 'protection.failed',
|
18
|
+
html_types: %w[text/html application/xhtml text/xml application/xml]
|
17
19
|
}
|
18
20
|
|
19
21
|
attr_reader :app, :options
|
@@ -31,7 +33,8 @@ module Rack
|
|
31
33
|
end
|
32
34
|
|
33
35
|
def initialize(app, options = {})
|
34
|
-
@app
|
36
|
+
@app = app
|
37
|
+
@options = default_options.merge(options)
|
35
38
|
end
|
36
39
|
|
37
40
|
def safe?(env)
|
@@ -52,24 +55,26 @@ module Rack
|
|
52
55
|
|
53
56
|
def react(env)
|
54
57
|
result = send(options[:reaction], env)
|
55
|
-
result if Array === result
|
58
|
+
result if (Array === result) && (result.size == 3)
|
56
59
|
end
|
57
60
|
|
58
61
|
def warn(env, message)
|
59
62
|
return unless options[:logging]
|
63
|
+
|
60
64
|
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
|
61
65
|
l.warn(message)
|
62
66
|
end
|
63
67
|
|
64
68
|
def instrument(env)
|
65
|
-
return unless i = options[:instrumenter]
|
69
|
+
return unless (i = options[:instrumenter])
|
70
|
+
|
66
71
|
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
|
67
72
|
i.instrument('rack.protection', env)
|
68
73
|
end
|
69
74
|
|
70
75
|
def deny(env)
|
71
76
|
warn env, "attack prevented by #{self.class}"
|
72
|
-
[options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
|
77
|
+
[options[:status], { 'Content-Type' => 'text/plain' }, [options[:message]]]
|
73
78
|
end
|
74
79
|
|
75
80
|
def report(env)
|
@@ -83,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
|
@@ -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 = []
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/protection'
|
2
4
|
require 'pathname'
|
3
5
|
|
@@ -29,9 +31,8 @@ module Rack
|
|
29
31
|
cookie_header = env['HTTP_COOKIE']
|
30
32
|
cookies = Rack::Utils.parse_query(cookie_header, ';,') { |s| s }
|
31
33
|
cookies.each do |k, v|
|
32
|
-
if k == session_key && Array(v).size > 1
|
33
|
-
|
34
|
-
elsif k != session_key && Rack::Utils.unescape(k) == session_key
|
34
|
+
if (k == session_key && Array(v).size > 1) ||
|
35
|
+
(k != session_key && Rack::Utils.unescape(k) == session_key)
|
35
36
|
bad_cookies << k
|
36
37
|
end
|
37
38
|
end
|
@@ -40,6 +41,7 @@ module Rack
|
|
40
41
|
|
41
42
|
def remove_bad_cookies(request, response)
|
42
43
|
return if bad_cookies.empty?
|
44
|
+
|
43
45
|
paths = cookie_paths(request.path)
|
44
46
|
bad_cookies.each do |name|
|
45
47
|
paths.each { |path| response.set_cookie name, empty_cookie(request.host, path) }
|
@@ -49,7 +51,7 @@ module Rack
|
|
49
51
|
def redirect(env)
|
50
52
|
request = Request.new(env)
|
51
53
|
warn env, "attack prevented by #{self.class}"
|
52
|
-
[302, {'Content-Type' => 'text/html', 'Location' => request.path}, []]
|
54
|
+
[302, { 'Content-Type' => 'text/html', 'Location' => request.path }, []]
|
53
55
|
end
|
54
56
|
|
55
57
|
def bad_cookies
|
@@ -64,7 +66,7 @@ module Rack
|
|
64
66
|
end
|
65
67
|
|
66
68
|
def empty_cookie(host, path)
|
67
|
-
{:
|
69
|
+
{ value: '', domain: host, path: path, expires: Time.at(0) }
|
68
70
|
end
|
69
71
|
|
70
72
|
def session_key
|