actionpack 8.1.4 → 8.1.4.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/CHANGELOG.md +94 -0
- data/lib/action_controller/metal/live.rb +10 -4
- data/lib/action_controller/metal/rendering.rb +9 -2
- data/lib/action_controller/metal/strong_parameters.rb +16 -8
- data/lib/action_dispatch/http/mime_negotiation.rb +11 -1
- data/lib/action_dispatch/middleware/cookies.rb +16 -1
- data/lib/action_dispatch/middleware/remote_ip.rb +52 -21
- data/lib/action_dispatch/middleware/session/abstract_store.rb +5 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +26 -3
- data/lib/action_dispatch/middleware/session/cookie_store.rb +0 -3
- data/lib/action_pack/gem_version.rb +1 -1
- metadata +10 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b25f91effbacce23d46488213ff6dc4e2d652894ee983905b167f3ae1d6908fb
|
|
4
|
+
data.tar.gz: f59e9e73d8ffdce6011e0e25c07cc660c9bf448b73a093899dd24a6f542a1b56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 26f0934c47f534dbf4739421b6661264a0d13d13d4e6cec16c3bf55708911cd146ff9b0ab07db125623367032e9ec68be4837d22982c1fc30c0d3f71af9d33e8
|
|
7
|
+
data.tar.gz: df92e22c98d46954a0064e3b62affa5b632f384a70ee00f750c45d375b61a553e95c859734fb9fd601bf35fe6b67247fade376d6a9dea355d133bd03b22082b4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,97 @@
|
|
|
1
|
+
## Rails 8.1.4.1 (November 03, 2026) ##
|
|
2
|
+
|
|
3
|
+
* Apply `config.action_dispatch.cookies_same_site_protection` to every session
|
|
4
|
+
store.
|
|
5
|
+
|
|
6
|
+
`ActionDispatch::Session::CacheStore`, `MemCacheStore` and third-party
|
|
7
|
+
stores built on `AbstractStore` or the session mixins sent session cookies
|
|
8
|
+
with no `SameSite` attribute. Only `CookieStore` applied the configured
|
|
9
|
+
default.
|
|
10
|
+
|
|
11
|
+
[GHSA-gmj8-hvj3-49mw]
|
|
12
|
+
|
|
13
|
+
*Jean Boussier*
|
|
14
|
+
|
|
15
|
+
* Prevent `ActionController::Live` streams from blocking when a full buffer is
|
|
16
|
+
closed or aborted.
|
|
17
|
+
|
|
18
|
+
[GHSA-rwvh-fw6r-pwvp]
|
|
19
|
+
|
|
20
|
+
*Jeremy Daer*
|
|
21
|
+
|
|
22
|
+
* Set and extend the `Vary` header for rendered responses according to the
|
|
23
|
+
request headers used in format negotiation.
|
|
24
|
+
|
|
25
|
+
`Vary: Accept` was only added when the request carried a usable `Accept`
|
|
26
|
+
header. A response to a request without one, negotiated to HTML or
|
|
27
|
+
JavaScript, could be stored by a shared cache and served to later clients
|
|
28
|
+
that requested a different representation.
|
|
29
|
+
|
|
30
|
+
Responses now vary on `Accept` and `X-Requested-With` whenever the format is
|
|
31
|
+
not given by the URL. XHR requests without `Accept` also vary on
|
|
32
|
+
`Content-Type`, which Rails uses as a legacy format fallback. When
|
|
33
|
+
`config.action_dispatch.ignore_accept_header` is enabled, responses vary
|
|
34
|
+
only on `X-Requested-With`.
|
|
35
|
+
|
|
36
|
+
These fields are appended to an existing `Vary` value without duplicating
|
|
37
|
+
fields case-insensitively. `Vary: *` remains unchanged.
|
|
38
|
+
|
|
39
|
+
[GHSA-r9jg-fmfx-2gv6]
|
|
40
|
+
|
|
41
|
+
*Jeremy Daer*
|
|
42
|
+
|
|
43
|
+
* Skip writing a `CacheStore` session that another request deleted while this
|
|
44
|
+
one ran.
|
|
45
|
+
|
|
46
|
+
A request that loaded a session from `ActionDispatch::Session::CacheStore`
|
|
47
|
+
wrote it back when it finished, even after a concurrent `reset_session` or a
|
|
48
|
+
server-side delete had removed it, so the old session ID worked again.
|
|
49
|
+
|
|
50
|
+
[GHSA-2586-8ghr-69fj]
|
|
51
|
+
|
|
52
|
+
*Mike Dalessio*
|
|
53
|
+
|
|
54
|
+
* Don't write upgraded cookies to a response that skips the session.
|
|
55
|
+
|
|
56
|
+
Reading a signed or encrypted cookie written under a rotated secret, or in
|
|
57
|
+
the format a `:hybrid` serializer migrates from, writes it back in the
|
|
58
|
+
current format. That write ignored `request.session_options[:skip]`, so a
|
|
59
|
+
response that skips the session, such as the Active Storage proxy's
|
|
60
|
+
public, cacheable response, could still carry the session cookie in
|
|
61
|
+
`Set-Cookie`. Such a response now leaves the cookie unchanged, and a later
|
|
62
|
+
request that does not skip the session upgrades it. An application that
|
|
63
|
+
skips the session on every request no longer upgrades its signed or
|
|
64
|
+
encrypted cookies. Cookies the application sets are still written.
|
|
65
|
+
|
|
66
|
+
[GHSA-x6mj-8jx9-qhqv]
|
|
67
|
+
|
|
68
|
+
*Mike Dalessio*
|
|
69
|
+
|
|
70
|
+
* Detect conflicts between the `Forwarded` and `X-Forwarded-For` headers in
|
|
71
|
+
`ActionDispatch::RemoteIp`.
|
|
72
|
+
|
|
73
|
+
When `ip_spoofing_check` is enabled (the default), requests carrying both
|
|
74
|
+
headers with conflicting client addresses now raise
|
|
75
|
+
`ActionDispatch::RemoteIp::IpSpoofAttackError`.
|
|
76
|
+
|
|
77
|
+
[GHSA-2gff-2mmg-m73p]
|
|
78
|
+
|
|
79
|
+
*Gannon McGibbon*, *Mike Dalessio*
|
|
80
|
+
|
|
81
|
+
* `ActionController::Parameters#each_key` returns `self` when given a block.
|
|
82
|
+
|
|
83
|
+
It returned the internal `ActiveSupport::HashWithIndifferentAccess`, which
|
|
84
|
+
does not respond to `permitted?`, so passing that return value to mass
|
|
85
|
+
assignment skipped the Strong Parameters check. `each`, `each_pair` and
|
|
86
|
+
`each_value` already return `self`.
|
|
87
|
+
|
|
88
|
+
Document that `ActionController::Parameters#as_json` returns every
|
|
89
|
+
parameter, permitted or not, in a hash that mass assignment does not check.
|
|
90
|
+
|
|
91
|
+
[GHSA-wm3x-v3qw-4jh3]
|
|
92
|
+
|
|
93
|
+
*Mike Dalessio*
|
|
94
|
+
|
|
1
95
|
## Rails 8.1.4 (September 24, 2026) ##
|
|
2
96
|
|
|
3
97
|
* Fix the debug error page to not attempt to read source fragments from
|
|
@@ -221,7 +221,11 @@ module ActionController
|
|
|
221
221
|
@response.delete_header "Content-Length"
|
|
222
222
|
end
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
begin
|
|
225
|
+
super
|
|
226
|
+
rescue ClosedQueueError
|
|
227
|
+
raise IOError, "closed stream" if connected?
|
|
228
|
+
end
|
|
225
229
|
|
|
226
230
|
unless connected?
|
|
227
231
|
@buf.clear
|
|
@@ -241,14 +245,14 @@ module ActionController
|
|
|
241
245
|
write string.end_with?("\n") ? string : "#{string}\n"
|
|
242
246
|
end
|
|
243
247
|
|
|
244
|
-
#
|
|
245
|
-
#
|
|
248
|
+
# Close the buffer; the producer/writing thread uses this to notify us that
|
|
249
|
+
# it's finished supplying content.
|
|
246
250
|
#
|
|
247
251
|
# See also #abort.
|
|
248
252
|
def close
|
|
249
253
|
synchronize do
|
|
250
254
|
super
|
|
251
|
-
@buf.
|
|
255
|
+
@buf.close
|
|
252
256
|
@cv.broadcast
|
|
253
257
|
end
|
|
254
258
|
end
|
|
@@ -260,7 +264,9 @@ module ActionController
|
|
|
260
264
|
def abort
|
|
261
265
|
synchronize do
|
|
262
266
|
@aborted = true
|
|
267
|
+
@buf.close
|
|
263
268
|
@buf.clear
|
|
269
|
+
@cv.broadcast
|
|
264
270
|
end
|
|
265
271
|
end
|
|
266
272
|
|
|
@@ -224,8 +224,15 @@ module ActionController
|
|
|
224
224
|
end
|
|
225
225
|
|
|
226
226
|
def _set_vary_header
|
|
227
|
-
if
|
|
228
|
-
response.headers["Vary"]
|
|
227
|
+
if request.should_apply_vary_header?
|
|
228
|
+
vary_headers = response.headers["Vary"].to_s.split(",").map(&:strip).reject(&:blank?)
|
|
229
|
+
|
|
230
|
+
unless vary_headers.include?("*")
|
|
231
|
+
request.vary_headers.each do |header|
|
|
232
|
+
vary_headers << header unless vary_headers.any? { |existing| existing.casecmp?(header) }
|
|
233
|
+
end
|
|
234
|
+
response.headers["Vary"] = vary_headers.join(", ")
|
|
235
|
+
end
|
|
229
236
|
end
|
|
230
237
|
end
|
|
231
238
|
|
|
@@ -197,15 +197,14 @@ module ActionController
|
|
|
197
197
|
# as_json(options=nil)
|
|
198
198
|
#
|
|
199
199
|
# Returns a hash that can be used as the JSON representation for the parameters.
|
|
200
|
-
|
|
201
|
-
##
|
|
202
|
-
# :method: each_key
|
|
203
200
|
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
201
|
+
# Like `#to_unsafe_h`, the hash holds every parameter, permitted or not, and
|
|
202
|
+
# mass assignment does not check it. Do not pass it to a model.
|
|
206
203
|
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
204
|
+
# params = ActionController::Parameters.new(name: "Bob", admin: true)
|
|
205
|
+
# params.as_json # => {"name" => "Bob", "admin" => true}
|
|
206
|
+
# User.new(params) # => ActiveModel::ForbiddenAttributesError
|
|
207
|
+
# User.new(params.as_json) # => #<User name: "Bob", admin: true>
|
|
209
208
|
|
|
210
209
|
##
|
|
211
210
|
# :method: empty?
|
|
@@ -248,7 +247,7 @@ module ActionController
|
|
|
248
247
|
# Returns the content of the parameters as a string.
|
|
249
248
|
|
|
250
249
|
delegate :keys, :empty?, :exclude?, :include?,
|
|
251
|
-
:as_json, :to_s,
|
|
250
|
+
:as_json, :to_s, to: :@parameters
|
|
252
251
|
|
|
253
252
|
alias_method :has_key?, :include?
|
|
254
253
|
alias_method :key?, :include?
|
|
@@ -420,6 +419,15 @@ module ActionController
|
|
|
420
419
|
self
|
|
421
420
|
end
|
|
422
421
|
|
|
422
|
+
# Calls block once for each key in the parameters, passing the key. If no block
|
|
423
|
+
# is given, an enumerator is returned instead.
|
|
424
|
+
def each_key(&block)
|
|
425
|
+
return to_enum(:each_key) unless block_given?
|
|
426
|
+
@parameters.each_key(&block)
|
|
427
|
+
|
|
428
|
+
self
|
|
429
|
+
end
|
|
430
|
+
|
|
423
431
|
# Returns a new array of the values of the parameters.
|
|
424
432
|
def values
|
|
425
433
|
to_enum(:each_value).to_a
|
|
@@ -212,7 +212,17 @@ module ActionDispatch
|
|
|
212
212
|
end
|
|
213
213
|
|
|
214
214
|
def should_apply_vary_header?
|
|
215
|
-
!params_readable?
|
|
215
|
+
!params_readable?
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def vary_headers # :nodoc:
|
|
219
|
+
if use_accept_header
|
|
220
|
+
headers = ["Accept", "X-Requested-With"]
|
|
221
|
+
headers.insert(1, "Content-Type") if xhr? && accept.blank?
|
|
222
|
+
headers
|
|
223
|
+
else
|
|
224
|
+
["X-Requested-With"]
|
|
225
|
+
end
|
|
216
226
|
end
|
|
217
227
|
|
|
218
228
|
private
|
|
@@ -324,6 +324,7 @@ module ActionDispatch
|
|
|
324
324
|
def initialize(request)
|
|
325
325
|
@set_cookies = {}
|
|
326
326
|
@delete_cookies = {}
|
|
327
|
+
@upgraded_cookies = Set.new
|
|
327
328
|
@request = request
|
|
328
329
|
@cookies = {}
|
|
329
330
|
@committed = false
|
|
@@ -386,6 +387,7 @@ module ActionDispatch
|
|
|
386
387
|
end
|
|
387
388
|
|
|
388
389
|
handle_options(options)
|
|
390
|
+
@upgraded_cookies.delete(name.to_s)
|
|
389
391
|
|
|
390
392
|
if @cookies[name.to_s] != value || options[:expires]
|
|
391
393
|
@cookies[name.to_s] = value
|
|
@@ -426,8 +428,14 @@ module ActionDispatch
|
|
|
426
428
|
@cookies.each_key { |k| delete(k, options) }
|
|
427
429
|
end
|
|
428
430
|
|
|
431
|
+
def upgraded(name)
|
|
432
|
+
@upgraded_cookies << name.to_s
|
|
433
|
+
end
|
|
434
|
+
|
|
429
435
|
def write(response)
|
|
430
436
|
@set_cookies.each do |name, value|
|
|
437
|
+
next if @upgraded_cookies.include?(name) && request.session_options[:skip]
|
|
438
|
+
|
|
431
439
|
if write_cookie?(value)
|
|
432
440
|
response.set_cookie(name, value)
|
|
433
441
|
end
|
|
@@ -533,6 +541,10 @@ module ActionDispatch
|
|
|
533
541
|
@parent_jar[name] = options
|
|
534
542
|
end
|
|
535
543
|
|
|
544
|
+
def upgraded(name)
|
|
545
|
+
@parent_jar.upgraded(name)
|
|
546
|
+
end
|
|
547
|
+
|
|
536
548
|
protected
|
|
537
549
|
def request; @parent_jar.request; end
|
|
538
550
|
|
|
@@ -599,7 +611,10 @@ module ActionDispatch
|
|
|
599
611
|
return
|
|
600
612
|
end
|
|
601
613
|
|
|
602
|
-
|
|
614
|
+
if force_reserialize || reserialize?(dumped)
|
|
615
|
+
self[name] = { value: value }
|
|
616
|
+
upgraded(name)
|
|
617
|
+
end
|
|
603
618
|
|
|
604
619
|
value
|
|
605
620
|
end
|
|
@@ -26,10 +26,10 @@ module ActionDispatch
|
|
|
26
26
|
# middleware assumes that there is at least one proxy sitting around and setting
|
|
27
27
|
# headers with the client's remote IP address. If you don't use a proxy, because
|
|
28
28
|
# you are hosted on e.g. Heroku without SSL, any client can claim to have any IP
|
|
29
|
-
# address by setting the `X-Forwarded-For` header. If you care
|
|
30
|
-
# you need to explicitly drop or ignore those headers sometime
|
|
31
|
-
# middleware runs. Alternatively, remove this middleware to avoid
|
|
32
|
-
# relying on it.
|
|
29
|
+
# address by setting the `Forwarded` or `X-Forwarded-For` header. If you care
|
|
30
|
+
# about that, then you need to explicitly drop or ignore those headers sometime
|
|
31
|
+
# before this middleware runs. Alternatively, remove this middleware to avoid
|
|
32
|
+
# inadvertently relying on it.
|
|
33
33
|
class RemoteIp
|
|
34
34
|
class IpSpoofAttackError < StandardError; end
|
|
35
35
|
|
|
@@ -53,10 +53,11 @@ module ActionDispatch
|
|
|
53
53
|
# Create a new `RemoteIp` middleware instance.
|
|
54
54
|
#
|
|
55
55
|
# The `ip_spoofing_check` option is on by default. When on, an exception is
|
|
56
|
-
# raised if it looks like the client is trying to lie about its own IP address
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
56
|
+
# raised if it looks like the client is trying to lie about its own IP address,
|
|
57
|
+
# such as when the request carries conflicting `Forwarded`, `X-Forwarded-For`,
|
|
58
|
+
# or `Client-Ip` headers. It makes sense to turn off this check on sites aimed
|
|
59
|
+
# at non-IP clients (like WAP devices), or behind proxies that set headers in
|
|
60
|
+
# an incorrect or confusing way (like AWS ELB).
|
|
60
61
|
#
|
|
61
62
|
# The `custom_proxies` argument can take an enumerable which will be used
|
|
62
63
|
# instead of `TRUSTED_PROXIES`. Any proxy setup will put the value you want in
|
|
@@ -102,6 +103,14 @@ module ActionDispatch
|
|
|
102
103
|
# an actual IP address. If the ActionDispatch::Request#remote_ip method is
|
|
103
104
|
# called, this class will calculate the value and then memoize it.
|
|
104
105
|
class GetIp
|
|
106
|
+
# Parses X-Forwarded-For as Rack does, whatever `Rack::Request.forwarded_priority` says.
|
|
107
|
+
class XForwardedForParser < Rack::Request # :nodoc:
|
|
108
|
+
private
|
|
109
|
+
def forwarded_priority
|
|
110
|
+
[:x_forwarded]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
105
114
|
def initialize(req, check_ip, proxies)
|
|
106
115
|
@req = req
|
|
107
116
|
@check_ip = check_ip
|
|
@@ -134,27 +143,20 @@ module ActionDispatch
|
|
|
134
143
|
client_ips = sanitize_ips(ips_from(@req.client_ip)).reverse!
|
|
135
144
|
forwarded_ips = sanitize_ips(@req.forwarded_for || []).reverse!
|
|
136
145
|
|
|
137
|
-
# `Client-Ip` and `X-Forwarded-For` should not, generally,
|
|
138
|
-
# are
|
|
146
|
+
# `Client-Ip`, `Forwarded`, and `X-Forwarded-For` should not, generally, all
|
|
147
|
+
# be set. If several of them are set, it means that either:
|
|
139
148
|
#
|
|
140
|
-
# 1) This request passed through
|
|
149
|
+
# 1) This request passed through proxies with incompatible IP header
|
|
141
150
|
# conventions.
|
|
142
151
|
#
|
|
143
|
-
# 2) The client passed
|
|
144
|
-
#
|
|
152
|
+
# 2) The client passed the headers the proxy servers weren't using
|
|
153
|
+
# themselves.
|
|
145
154
|
#
|
|
146
155
|
# Either way, there is no way for us to determine which header is the right one
|
|
147
156
|
# after the fact. Since we have no idea, if we are concerned about IP spoofing
|
|
148
157
|
# we need to give up and explode. (If you're not concerned about IP spoofing you
|
|
149
158
|
# can turn the `ip_spoofing_check` option off.)
|
|
150
|
-
|
|
151
|
-
if should_check_ip && !forwarded_ips.include?(client_ips.last)
|
|
152
|
-
# We don't know which came from the proxy, and which from the user
|
|
153
|
-
raise IpSpoofAttackError, "IP spoofing attack?! " \
|
|
154
|
-
"HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
|
|
155
|
-
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" \
|
|
156
|
-
" HTTP_FORWARDED=" + @req.forwarded_for.map { "for=#{_1}" }.join(", ").inspect if @req.forwarded_for.any?
|
|
157
|
-
end
|
|
159
|
+
check_ip(client_ips, forwarded_ips) if @check_ip
|
|
158
160
|
|
|
159
161
|
# We assume these things about the IP headers:
|
|
160
162
|
#
|
|
@@ -182,6 +184,35 @@ module ActionDispatch
|
|
|
182
184
|
header.strip.split(/[,\s]+/)
|
|
183
185
|
end
|
|
184
186
|
|
|
187
|
+
def raise_spoofing_error
|
|
188
|
+
message = +"IP spoofing attack?! " \
|
|
189
|
+
"HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
|
|
190
|
+
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
|
|
191
|
+
if forwarded = @req.get_header("HTTP_FORWARDED")
|
|
192
|
+
message << " HTTP_FORWARDED=#{forwarded.inspect}"
|
|
193
|
+
end
|
|
194
|
+
raise IpSpoofAttackError, message
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def check_ip(client_ips, forwarded_ips)
|
|
198
|
+
if client_ips.last && forwarded_ips.last && !forwarded_ips.include?(client_ips.last)
|
|
199
|
+
# We don't know which came from the proxy, and which from the user
|
|
200
|
+
raise_spoofing_error
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if @req.get_header("HTTP_FORWARDED") && @req.x_forwarded_for
|
|
204
|
+
x_forwarded_ips = XForwardedForParser.new("HTTP_X_FORWARDED_FOR" => @req.x_forwarded_for).forwarded_for || []
|
|
205
|
+
x_forwarded_client = filter_proxies(sanitize_ips(x_forwarded_ips).reverse!).first
|
|
206
|
+
forwarded_client = filter_proxies(forwarded_ips).first
|
|
207
|
+
|
|
208
|
+
# Compare strings, not parsed addresses, so the client cannot choose how its
|
|
209
|
+
# own address is spelled.
|
|
210
|
+
if x_forwarded_client && x_forwarded_client != forwarded_client
|
|
211
|
+
raise_spoofing_error
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
185
216
|
def sanitize_ips(ips) # :doc:
|
|
186
217
|
ips.select! do |ip|
|
|
187
218
|
# Only return IPs that are valid according to the IPAddr#new method.
|
|
@@ -20,8 +20,13 @@ module ActionDispatch
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
module Compatibility
|
|
23
|
+
DEFAULT_SAME_SITE = proc { |request| request.cookies_same_site_protection } # :nodoc:
|
|
24
|
+
|
|
23
25
|
def initialize(app, options = {})
|
|
24
26
|
options[:key] ||= "_session_id"
|
|
27
|
+
unless options.key?(:same_site) || self.class::DEFAULT_OPTIONS.key?(:same_site)
|
|
28
|
+
options = options.merge(same_site: DEFAULT_SAME_SITE)
|
|
29
|
+
end
|
|
25
30
|
super
|
|
26
31
|
end
|
|
27
32
|
|
|
@@ -24,6 +24,8 @@ module ActionDispatch
|
|
|
24
24
|
# By default, it is set to `false` to avoid additional cache write operations.
|
|
25
25
|
#
|
|
26
26
|
class CacheStore < AbstractSecureStore
|
|
27
|
+
LOADED_SESSION_ID = "action_dispatch.session.cache_store.loaded_session_id" # :nodoc:
|
|
28
|
+
|
|
27
29
|
def initialize(app, options = {})
|
|
28
30
|
@cache = options[:cache] || Rails.cache
|
|
29
31
|
options[:expire_after] ||= @cache.options[:expires_in]
|
|
@@ -32,17 +34,20 @@ module ActionDispatch
|
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Get a session from the cache.
|
|
35
|
-
def find_session(
|
|
36
|
-
|
|
37
|
+
def find_session(req, sid)
|
|
38
|
+
if sid && (session = get_session_with_fallback(sid))
|
|
39
|
+
req.set_header(LOADED_SESSION_ID, sid.private_id)
|
|
40
|
+
else
|
|
37
41
|
sid, session = generate_sid, {}
|
|
38
42
|
end
|
|
39
43
|
[sid, session]
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
# Set a session in the cache.
|
|
43
|
-
def write_session(
|
|
47
|
+
def write_session(req, sid, session, options)
|
|
44
48
|
key = cache_key(sid.private_id)
|
|
45
49
|
if session
|
|
50
|
+
return false if destroyed_since_loaded?(req, sid)
|
|
46
51
|
@cache.write(key, session, expires_in: options[:expire_after])
|
|
47
52
|
else
|
|
48
53
|
@cache.delete(key)
|
|
@@ -67,6 +72,24 @@ module ActionDispatch
|
|
|
67
72
|
@cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
|
|
68
73
|
end
|
|
69
74
|
|
|
75
|
+
# Writing back a session that another request deleted, such as on logout,
|
|
76
|
+
# would restore it.
|
|
77
|
+
def destroyed_since_loaded?(req, sid)
|
|
78
|
+
req.get_header(LOADED_SESSION_ID) == sid.private_id &&
|
|
79
|
+
!exist_in_backend?(cache_key(sid.private_id)) &&
|
|
80
|
+
!exist_in_backend?(cache_key(sid.public_id))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The request-local cache still holds the entry this request loaded, so
|
|
84
|
+
# a lookup through it cannot see a delete made by another request.
|
|
85
|
+
def exist_in_backend?(key)
|
|
86
|
+
if @cache.respond_to?(:with_local_cache)
|
|
87
|
+
@cache.with_local_cache { @cache.exist?(key) }
|
|
88
|
+
else
|
|
89
|
+
@cache.exist?(key)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
70
93
|
def generate_sid
|
|
71
94
|
if @check_collisions
|
|
72
95
|
loop do
|
|
@@ -59,11 +59,8 @@ module ActionDispatch
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
DEFAULT_SAME_SITE = proc { |request| request.cookies_same_site_protection } # :nodoc:
|
|
63
|
-
|
|
64
62
|
def initialize(app, options = {})
|
|
65
63
|
options[:cookie_only] = true
|
|
66
|
-
options[:same_site] = DEFAULT_SAME_SITE if !options.key?(:same_site)
|
|
67
64
|
super
|
|
68
65
|
end
|
|
69
66
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: actionpack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.1.4
|
|
4
|
+
version: 8.1.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Heinemeier Hansson
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 8.1.4
|
|
18
|
+
version: 8.1.4.1
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 8.1.4
|
|
25
|
+
version: 8.1.4.1
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: nokogiri
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -127,28 +127,28 @@ dependencies:
|
|
|
127
127
|
requirements:
|
|
128
128
|
- - '='
|
|
129
129
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: 8.1.4
|
|
130
|
+
version: 8.1.4.1
|
|
131
131
|
type: :runtime
|
|
132
132
|
prerelease: false
|
|
133
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
134
|
requirements:
|
|
135
135
|
- - '='
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
|
-
version: 8.1.4
|
|
137
|
+
version: 8.1.4.1
|
|
138
138
|
- !ruby/object:Gem::Dependency
|
|
139
139
|
name: activemodel
|
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
|
141
141
|
requirements:
|
|
142
142
|
- - '='
|
|
143
143
|
- !ruby/object:Gem::Version
|
|
144
|
-
version: 8.1.4
|
|
144
|
+
version: 8.1.4.1
|
|
145
145
|
type: :development
|
|
146
146
|
prerelease: false
|
|
147
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
148
148
|
requirements:
|
|
149
149
|
- - '='
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
|
-
version: 8.1.4
|
|
151
|
+
version: 8.1.4.1
|
|
152
152
|
description: Web apps on Rails. Simple, battle-tested conventions for building and
|
|
153
153
|
testing MVC web applications. Works with any Rack-compatible server.
|
|
154
154
|
email: david@loudthinking.com
|
|
@@ -352,10 +352,10 @@ licenses:
|
|
|
352
352
|
- MIT
|
|
353
353
|
metadata:
|
|
354
354
|
bug_tracker_uri: https://github.com/rails/rails/issues
|
|
355
|
-
changelog_uri: https://github.com/rails/rails/blob/v8.1.4/actionpack/CHANGELOG.md
|
|
356
|
-
documentation_uri: https://api.rubyonrails.org/v8.1.4/
|
|
355
|
+
changelog_uri: https://github.com/rails/rails/blob/v8.1.4.1/actionpack/CHANGELOG.md
|
|
356
|
+
documentation_uri: https://api.rubyonrails.org/v8.1.4.1/
|
|
357
357
|
mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
|
|
358
|
-
source_code_uri: https://github.com/rails/rails/tree/v8.1.4/actionpack
|
|
358
|
+
source_code_uri: https://github.com/rails/rails/tree/v8.1.4.1/actionpack
|
|
359
359
|
rubygems_mfa_required: 'true'
|
|
360
360
|
rdoc_options: []
|
|
361
361
|
require_paths:
|