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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 188361aae9342d1758609c8aae8e654ed45555478396061ad363d516afaa9447
4
- data.tar.gz: a327bf62a9c720d6507c3ab0523e10c27042bac1c94bdbb738bb6dcfcf9b2255
3
+ metadata.gz: b25f91effbacce23d46488213ff6dc4e2d652894ee983905b167f3ae1d6908fb
4
+ data.tar.gz: f59e9e73d8ffdce6011e0e25c07cc660c9bf448b73a093899dd24a6f542a1b56
5
5
  SHA512:
6
- metadata.gz: aff62143454ada2ba6226b429126b4ec52aa8e55afd690e3e35db8f1add1a63e661ec837ced36660ed02f2bd2b094a516401238f8a959cf792a7e2afc57928c7
7
- data.tar.gz: 32ff126c37ddfa3c381b374122cf4b636e6589d3cf16ca1cf4f231d0931d4ae0c0ef80ca13d5d2296988833a842b3b09d8ac89f22a72074230763b145758c3a2
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
- super
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
- # Write a 'close' event to the buffer; the producer/writing thread uses this to
245
- # notify us that it's finished supplying content.
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.push nil
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 response.headers["Vary"].blank? && request.should_apply_vary_header?
228
- response.headers["Vary"] = "Accept"
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
- # :call-seq:
205
- # each_key(&block)
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
- # Calls block once for each key in the parameters, passing the key. If no block
208
- # is given, an enumerator is returned instead.
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, :each_key, to: :@parameters
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? && use_accept_header && valid_accept_header
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
- self[name] = { value: value } if force_reserialize || reserialize?(dumped)
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 about that, then
30
- # you need to explicitly drop or ignore those headers sometime before this
31
- # middleware runs. Alternatively, remove this middleware to avoid inadvertently
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
- # It makes sense to turn off this check on sites aimed at non-IP clients (like
58
- # WAP devices), or behind proxies that set headers in an incorrect or confusing
59
- # way (like AWS ELB).
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, both be set. If they
138
- # are both set, it means that either:
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 two proxies with incompatible IP header
149
+ # 1) This request passed through proxies with incompatible IP header
141
150
  # conventions.
142
151
  #
143
- # 2) The client passed one of `Client-Ip` or `X-Forwarded-For`
144
- # (whichever the proxy servers weren't using) themselves.
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
- should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
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(env, sid)
36
- unless sid && (session = get_session_with_fallback(sid))
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(env, sid, session, options)
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
 
@@ -12,7 +12,7 @@ module ActionPack
12
12
  MAJOR = 8
13
13
  MINOR = 1
14
14
  TINY = 4
15
- PRE = nil
15
+ PRE = "1"
16
16
 
17
17
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
18
18
  end
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: