shrine 3.10.1 → 3.10.2

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: '078c4711b7bd211c577666899edb233967df970a48b96d3b6239189f9c516586'
4
- data.tar.gz: a46ed9608642d54c84fd80cc106e56655b4bff7439ff3cf2627bd1c199b40a15
3
+ metadata.gz: 2027c3833ffb021cf9fe9be3761c3238ca807eb1591ed910b585b226cad80497
4
+ data.tar.gz: fd9ce3716ac5c110a306f4823acf90de7cf673132ef90b181e8d7040cc8d74d0
5
5
  SHA512:
6
- metadata.gz: 9526af08df019ad15b330e804d2136d0a2f876657b55018b3498ec5fd9c75b881ee1d7559fb4bd862512975d0a7439c4991d11dedf61b9c2bcc6d8c1c3ef38b6
7
- data.tar.gz: af84847b2664f400250142bda5995b31d9303de422d5e73d75593ec2d9911f1cdc9e6dc27f10a3f97ab97aaefcb9ebf3f5754a35a6220501af2230dc7f7dcd6b
6
+ metadata.gz: 9181db3a505b16fba406a6d98cc2f4a37222fcbacdf47427d5ec80d087ef3746b0db2152d21685b89f8cfc77db766b0ed13f1b976241354dc48b9e82aaa1fd35
7
+ data.tar.gz: 4f891d6fcfd5cdd29a39e04479d42d20bc7a6511557a2f82f85dc6889536bf38be9b5f281389fe6751ce7e26271bd1c85897c6e899b6ba3e08d3070ffc1e717e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 3.10.2 (2026-10-11)
2
+
3
+ * `derivation_endpoint` – Prevent bypassing CDN caching by modifying the encoding or structure of the signed URL query string (@Antsiscool, @janko)
4
+
1
5
  ## 3.10.1 (2026-10-10)
2
6
 
3
7
  * `model` – Make `Attacher#write` a no-op when the attacher isn't backed by a model, fixing `remove_invalid` raising `NoMethodError` on attachers without a record (@janko)
@@ -0,0 +1,31 @@
1
+ ---
2
+ title: Shrine 3.10.2
3
+ ---
4
+
5
+ ## Security
6
+
7
+ * The `derivation_endpoint` plugin now verifies the URL signature against the
8
+ URL exactly as it was received. Previously, query parameters were decoded and
9
+ re-encoded before verification, so the same signed URL could be requested
10
+ through an unlimited number of variants that all passed verification. Since
11
+ each variant is a separate cache key on a CDN, this could be used to bypass
12
+ CDN caching and repeatedly trigger derivations on the server:
13
+
14
+ ```
15
+ /derivations/thumbnail/...?type=image%2Fwebp&signature=abc123 # original
16
+ /derivations/thumbnail/...?%74ype=image%2Fwebp&signature=abc123 # encoded parameter name
17
+ /derivations/thumbnail/...?&&type=image%2Fwebp&signature=abc123 # empty parameters
18
+ /derivations/thumbnail/...?type=image%2Fwebp&signature=abc123?foo # appended query
19
+ ```
20
+
21
+ These variants now return a `403 Forbidden` response. The `signature` query
22
+ parameter is also required to be the last parameter, which is where Shrine
23
+ always puts it.
24
+
25
+ ## Backwards compatibility
26
+
27
+ * URLs generated by the `derivation_endpoint` plugin are unchanged, so
28
+ existing URLs remain valid. However, if a proxy between the client and your
29
+ application rewrites the query string (e.g. re-encodes characters, reorders
30
+ parameters, or appends tracking parameters), signature verification will now
31
+ fail for those requests.
@@ -717,29 +717,27 @@ class Shrine
717
717
 
718
718
  # Returns a URL with the `signature` query parameter
719
719
  def sign_url(url)
720
- path, query = url.split("?")
720
+ path, query = url.split("?", 2)
721
721
 
722
- params = Rack::Utils.parse_query(query.to_s)
723
- params.merge!("signature" => generate_signature(url))
724
-
725
- query = Rack::Utils.build_query(params)
722
+ signature = generate_signature("#{path}?#{query}")
723
+ query = [query, "signature=#{signature}"].reject { |part| part.to_s.empty? }.join("&")
726
724
 
727
725
  "#{path}?#{query}"
728
726
  end
729
727
 
730
- # Calculcates the signature from the URL and checks whether it matches the
731
- # value in the `signature` query parameter. Raises `InvalidSignature` if
732
- # the `signature` parameter is missing or its value doesn't match the
733
- # calculated signature.
728
+ # Calculates the signature from the URL and checks whether it matches the
729
+ # value in the trailing `signature` query parameter. Raises
730
+ # `InvalidSignature` if the `signature` parameter is missing or its value
731
+ # doesn't match the calculated signature. The URL is verified as is,
732
+ # without any normalization, so that each signed URL has only one valid
733
+ # form (otherwise variants of it could be used to bypass CDN caching).
734
734
  def verify_url(url)
735
- path, query = url.split("?")
736
-
737
- params = Rack::Utils.parse_query(query.to_s)
738
- signature = params.delete("signature")
735
+ path, query = url.split("?", 2)
739
736
 
740
- query = Rack::Utils.build_query(params)
737
+ params = query.to_s.split("&", -1)
738
+ signature = params.pop.delete_prefix("signature=") if params.last&.start_with?("signature=")
741
739
 
742
- verify_signature("#{path}?#{query}", signature)
740
+ verify_signature("#{path}?#{params.join("&")}", signature)
743
741
  end
744
742
 
745
743
  def verify_signature(string, signature)
@@ -8,7 +8,7 @@ class Shrine
8
8
  module VERSION
9
9
  MAJOR = 3
10
10
  MINOR = 10
11
- TINY = 1
11
+ TINY = 2
12
12
  PRE = nil
13
13
 
14
14
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shrine
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.10.1
4
+ version: 3.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
@@ -491,6 +491,7 @@ files:
491
491
  - doc/release_notes/3.1.0.md
492
492
  - doc/release_notes/3.10.0.md
493
493
  - doc/release_notes/3.10.1.md
494
+ - doc/release_notes/3.10.2.md
494
495
  - doc/release_notes/3.2.0.md
495
496
  - doc/release_notes/3.2.1.md
496
497
  - doc/release_notes/3.2.2.md