Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package rubygem-rack-session for
openSUSE:Factory checked in at 2026-04-22 17:02:03
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rubygem-rack-session (Old)
and /work/SRC/openSUSE:Factory/.rubygem-rack-session.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rubygem-rack-session"
Wed Apr 22 17:02:03 2026 rev:4 rq:1348749 version:2.1.2
Changes:
--------
---
/work/SRC/openSUSE:Factory/rubygem-rack-session/rubygem-rack-session.changes
2025-10-10 17:09:46.326311997 +0200
+++
/work/SRC/openSUSE:Factory/.rubygem-rack-session.new.11940/rubygem-rack-session.changes
2026-04-22 17:02:37.003483098 +0200
@@ -1,0 +2,6 @@
+Wed Apr 22 08:37:09 UTC 2026 - Aleksei Burlakov <[email protected]>
+
+- New upstream release 2.1.2
+ * Fixed: crafted session cookie don't lead to gain unauthorized access
[CVE-2026-39324][bsc#1261831]
+
+-------------------------------------------------------------------
Old:
----
rack-session-2.1.1.gem
New:
----
rack-session-2.1.2.gem
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ rubygem-rack-session.spec ++++++
--- /var/tmp/diff_new_pack.45IVru/_old 2026-04-22 17:02:37.759513316 +0200
+++ /var/tmp/diff_new_pack.45IVru/_new 2026-04-22 17:02:37.767513636 +0200
@@ -1,7 +1,7 @@
#
# spec file for package rubygem-rack-session
#
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -24,7 +24,7 @@
#
Name: rubygem-rack-session
-Version: 2.1.1
+Version: 2.1.2
Release: 0
%define mod_name rack-session
%define mod_full_name %{mod_name}-%{version}
++++++ rack-session-2.1.1.gem -> rack-session-2.1.2.gem ++++++
Binary files old/checksums.yaml.gz and new/checksums.yaml.gz differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/lib/rack/session/cookie.rb
new/lib/rack/session/cookie.rb
--- old/lib/rack/session/cookie.rb 2025-05-06 12:54:56.000000000 +0200
+++ new/lib/rack/session/cookie.rb 1980-01-02 01:00:00.000000000 +0100
@@ -237,8 +237,10 @@
# Decode using legacy HMAC decoder
session_data = @legacy_hmac_coder.decode(session_data)
- elsif !session_data && coder
- # Use the coder option, which has the potential to be very unsafe
+ elsif !session_data && encryptors.empty? && coder
+ # Use the coder option, which has the potential to be very
unsafe.
+ # This path is only reached when no encryptors (secrets:) are
configured;
+ # if encryptors are present but decryption failed, the cookie is
rejected.
session_data = coder.decode(cookie_data)
end
end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/lib/rack/session/encryptor.rb
new/lib/rack/session/encryptor.rb
--- old/lib/rack/session/encryptor.rb 2025-05-06 12:54:56.000000000 +0200
+++ new/lib/rack/session/encryptor.rb 1980-01-02 01:00:00.000000000 +0100
@@ -5,9 +5,9 @@
# Copyright, 2022, by Philip Arndt.
require 'base64'
+require 'json'
require 'openssl'
require 'securerandom'
-require 'zlib'
require 'rack/utils'
@@ -23,169 +23,392 @@
class InvalidMessage < Error
end
- # The secret String must be at least 64 bytes in size. The first 32 bytes
- # will be used for the encryption cipher key. The remainder will be used
- # for an HMAC key.
- #
- # Options may include:
- # * :serialize_json
- # Use JSON for message serialization instead of Marshal. This can be
- # viewed as a security enhancement.
- # * :pad_size
- # Pad encrypted message data, to a multiple of this many bytes
- # (default: 32). This can be between 2-4096 bytes, or +nil+ to
disable
- # padding.
- # * :purpose
- # Limit messages to a specific purpose. This can be viewed as a
- # security enhancement to prevent message reuse from different
contexts
- # if keys are reused.
- #
- # Cryptography and Output Format:
- #
- # urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
- #
- # Where:
- # * version - 1 byte and is currently always 0x01
- # * random_data - 32 bytes used for generating the per-message secret
- # * IV - 16 bytes random initialization vector
- # * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
- # value
- def initialize(secret, opts = {})
- raise ArgumentError, "secret must be a String" unless String === secret
- raise ArgumentError, "invalid secret: #{secret.bytesize}, must be
>=64" unless secret.bytesize >= 64
+ module Serializable
+ private
+
+ # Returns a serialized payload of the message. If a :pad_size is
supplied,
+ # the message will be padded. The first 2 bytes of the returned string
will
+ # indicating the amount of padding.
+ def serialize_payload(message)
+ serialized_data = serializer.dump(message)
+
+ return
"#{[0].pack('v')}#{serialized_data.force_encoding(Encoding::BINARY)}" if
@options[:pad_size].nil?
+
+ padding_bytes = @options[:pad_size] - (2 + serialized_data.size) %
@options[:pad_size]
+ padding_data = SecureRandom.random_bytes(padding_bytes)
+
+
"#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data.force_encoding(Encoding::BINARY)}"
+ end
+
+ # Return the deserialized message. The first 2 bytes will be read as
the
+ # amount of padding.
+ def deserialized_message(data)
+ # Read the first 2 bytes as the padding_bytes size
+ padding_bytes, = data.unpack('v')
+
+ # Slice out the serialized_data and deserialize it
+ serialized_data = data.slice(2 + padding_bytes, data.bytesize)
+ serializer.load serialized_data
+ end
+
+ def serializer
+ @serializer ||= @options[:serialize_json] ? JSON : Marshal
+ end
+ end
+
+ class V1
+ include Serializable
- case opts[:pad_size]
- when nil
+ # The secret String must be at least 64 bytes in size. The first 32
bytes
+ # will be used for the encryption cipher key. The remainder will be
used
+ # for an HMAC key.
+ #
+ # Options may include:
+ # * :serialize_json
+ # Use JSON for message serialization instead of Marshal. This can
be
+ # viewed as a security enhancement.
+ # * :pad_size
+ # Pad encrypted message data, to a multiple of this many bytes
+ # (default: 32). This can be between 2-4096 bytes, or +nil+ to
disable
+ # padding.
+ # * :purpose
+ # Limit messages to a specific purpose. This can be viewed as a
+ # security enhancement to prevent message reuse from different
contexts
+ # if keys are reused.
+ #
+ # Cryptography and Output Format:
+ #
+ # urlsafe_encode64(version + random_data + IV + encrypted data +
HMAC)
+ #
+ # Where:
+ # * version - 1 byte with value 0x01
+ # * random_data - 32 bytes used for generating the per-message secret
+ # * IV - 16 bytes random initialization vector
+ # * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the
purpose
+ # value
+ def initialize(secret, opts = {})
+ raise ArgumentError, 'secret must be a String' unless
secret.is_a?(String)
+ raise ArgumentError, "invalid secret: #{secret.bytesize}, must be
>=64" unless secret.bytesize >= 64
+
+ case opts[:pad_size]
+ when nil
# padding is disabled
- when Integer
- raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless
(2..4096).include? opts[:pad_size]
- else
- raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be
Integer or nil"
+ when Integer
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless
(2..4096).include? opts[:pad_size]
+ else
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must
be Integer or nil"
+ end
+
+ @options = {
+ serialize_json: false, pad_size: 32, purpose: nil
+ }.update(opts)
+
+ @hmac_secret = secret.dup.force_encoding(Encoding::BINARY)
+ @cipher_secret = @hmac_secret.slice!(0, 32)
+
+ @hmac_secret.freeze
+ @cipher_secret.freeze
end
- @options = {
- serialize_json: false, pad_size: 32, purpose: nil
- }.update(opts)
+ def decrypt(base64_data)
+ data = Base64.urlsafe_decode64(base64_data)
- @hmac_secret = secret.dup.force_encoding('BINARY')
- @cipher_secret = @hmac_secret.slice!(0, 32)
+ signature = data.slice!(-32..-1)
+ verify_authenticity!(data, signature)
- @hmac_secret.freeze
- @cipher_secret.freeze
- end
+ version = data.slice!(0, 1)
+ raise InvalidMessage, 'wrong version' unless version == "\1"
- def decrypt(base64_data)
- data = Base64.urlsafe_decode64(base64_data)
+ message_secret = data.slice!(0, 32)
+ cipher_iv = data.slice!(0, 16)
- signature = data.slice!(-32..-1)
+ cipher = new_cipher
+ cipher.decrypt
- verify_authenticity! data, signature
+ set_cipher_key(cipher,
cipher_secret_from_message_secret(message_secret))
- # The version is reserved for future
- _version = data.slice!(0, 1)
- message_secret = data.slice!(0, 32)
- cipher_iv = data.slice!(0, 16)
+ cipher.iv = cipher_iv
+ data = cipher.update(data) << cipher.final
- cipher = new_cipher
- cipher.decrypt
+ deserialized_message data
+ rescue ArgumentError
+ raise InvalidSignature, 'Message invalid'
+ end
- set_cipher_key(cipher,
cipher_secret_from_message_secret(message_secret))
+ def encrypt(message)
+ version = "\1"
- cipher.iv = cipher_iv
- data = cipher.update(data) << cipher.final
+ serialized_payload = serialize_payload(message)
+ message_secret, cipher_secret = new_message_and_cipher_secret
- deserialized_message data
- rescue ArgumentError
- raise InvalidSignature, 'Message invalid'
- end
+ cipher = new_cipher
+ cipher.encrypt
- def encrypt(message)
- version = "\1"
+ set_cipher_key(cipher, cipher_secret)
- serialized_payload = serialize_payload(message)
- message_secret, cipher_secret = new_message_and_cipher_secret
+ cipher_iv = cipher.random_iv
- cipher = new_cipher
- cipher.encrypt
+ encrypted_data = cipher.update(serialized_payload) << cipher.final
- set_cipher_key(cipher, cipher_secret)
+ data = String.new
+ data << version
+ data << message_secret
+ data << cipher_iv
+ data << encrypted_data
+ data << compute_signature(data)
- cipher_iv = cipher.random_iv
+ Base64.urlsafe_encode64(data)
+ end
- encrypted_data = cipher.update(serialized_payload) << cipher.final
+ private
- data = String.new
- data << version
- data << message_secret
- data << cipher_iv
- data << encrypted_data
- data << compute_signature(data)
+ def new_cipher
+ OpenSSL::Cipher.new('aes-256-ctr')
+ end
- Base64.urlsafe_encode64(data)
- end
+ def new_message_and_cipher_secret
+ message_secret = SecureRandom.random_bytes(32)
- private
+ [message_secret, cipher_secret_from_message_secret(message_secret)]
+ end
- def new_cipher
- OpenSSL::Cipher.new('aes-256-ctr')
- end
+ def cipher_secret_from_message_secret(message_secret)
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @cipher_secret,
message_secret)
+ end
- def new_message_and_cipher_secret
- message_secret = SecureRandom.random_bytes(32)
+ def set_cipher_key(cipher, key)
+ cipher.key = key
+ end
- [message_secret, cipher_secret_from_message_secret(message_secret)]
- end
+ def compute_signature(data)
+ signing_data = data
+ signing_data += @options[:purpose] if @options[:purpose]
- def cipher_secret_from_message_secret(message_secret)
- OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @cipher_secret,
message_secret)
- end
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @hmac_secret,
signing_data)
+ end
- def set_cipher_key(cipher, key)
- cipher.key = key
- end
+ def verify_authenticity!(data, signature)
+ raise InvalidMessage, 'Message is invalid' if data.nil? ||
signature.nil?
- def serializer
- @serializer ||= @options[:serialize_json] ? JSON : Marshal
+ unless Rack::Utils.secure_compare(signature, compute_signature(data))
+ raise InvalidSignature, 'HMAC is invalid'
+ end
+ end
end
- def compute_signature(data)
- signing_data = data
- signing_data += @options[:purpose] if @options[:purpose]
+ class V2
+ include Serializable
+
+ # The secret String must be at least 32 bytes in size.
+ #
+ # Options may include:
+ # * :pad_size
+ # Pad encrypted message data, to a multiple of this many bytes
+ # (default: 32). This can be between 2-4096 bytes, or +nil+ to
disable
+ # padding.
+ # * :purpose
+ # Limit messages to a specific purpose. This can be viewed as a
+ # security enhancement to prevent message reuse from different
contexts
+ # if keys are reused.
+ #
+ # Cryptography and Output Format:
+ #
+ # strict_encode64(version + salt + IV + authentication tag +
ciphertext)
+ #
+ # Where:
+ # * version - 1 byte with value 0x02
+ # * salt - 32 bytes used for generating the per-message secret
+ # * IV - 12 bytes random initialization vector
+ # * authentication tag - 16 bytes authentication tag generated by the
GCM mode, covering version and salt
+ #
+ # Considerations about V2:
+ #
+ # 1) It uses non URL-safe Base64 encoding as it's faster than its
+ # URL-safe counterpart - as of Ruby 3.2, Base64.urlsafe_encode64 is
+ # roughly equivalent to
+ #
+ # Base64.strict_encode64(data).tr("-_", "+/")
+ #
+ # - and cookie values don't need to be URL-safe.
+ def initialize(secret, opts = {})
+ raise ArgumentError, 'secret must be a String' unless
secret.is_a?(String)
+
+ unless secret.bytesize >= 32
+ raise ArgumentError, "invalid secret: it's #{secret.bytesize}-byte
long, must be >=32"
+ end
- OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @hmac_secret,
signing_data)
+ case opts[:pad_size]
+ when nil
+ # padding is disabled
+ when Integer
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless
(2..4096).include? opts[:pad_size]
+ else
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must
be Integer or nil"
+ end
+
+ @options = {
+ serialize_json: false, pad_size: 32, purpose: nil
+ }.update(opts)
+
+ @cipher_secret =
secret.dup.force_encoding(Encoding::BINARY).slice!(0, 32)
+ @cipher_secret.freeze
+ end
+
+ def decrypt(base64_data)
+ data = Base64.strict_decode64(base64_data)
+ if data.bytesize <= 61 # version + salt + iv + auth_tag = 61 byte
(and we also need some ciphertext :)
+ raise InvalidMessage, 'invalid message'
+ end
+
+ version = data[0]
+ raise InvalidMessage, 'invalid message' unless version == "\2"
+
+ ciphertext = data.slice!(61..-1)
+ auth_tag = data.slice!(45, 16)
+ cipher_iv = data.slice!(33, 12)
+
+ cipher = new_cipher
+ cipher.decrypt
+ salt = data.slice(1, 32)
+ set_cipher_key(cipher, message_secret_from_salt(salt))
+ cipher.iv = cipher_iv
+ cipher.auth_tag = auth_tag
+ cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose :
data
+
+ plaintext = cipher.update(ciphertext) << cipher.final
+
+ deserialized_message plaintext
+ rescue ArgumentError, OpenSSL::Cipher::CipherError
+ raise InvalidSignature, 'invalid message'
+ end
+
+ def encrypt(message)
+ version = "\2"
+
+ serialized_payload = serialize_payload(message)
+
+ cipher = new_cipher
+ cipher.encrypt
+ salt, message_secret = new_salt_and_message_secret
+ set_cipher_key(cipher, message_secret)
+ cipher.iv_len = 12
+ cipher_iv = cipher.random_iv
+
+ data = String.new
+ data << version
+ data << salt
+
+ cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose :
data
+ encrypted_data = cipher.update(serialized_payload) << cipher.final
+
+ data << cipher_iv
+ data << auth_tag_from(cipher)
+ data << encrypted_data
+
+ Base64.strict_encode64(data)
+ end
+
+ private
+
+ def new_cipher
+ OpenSSL::Cipher.new('aes-256-gcm')
+ end
+
+ def new_salt_and_message_secret
+ salt = SecureRandom.random_bytes(32)
+
+ [salt, message_secret_from_salt(salt)]
+ end
+
+ def message_secret_from_salt(salt)
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @cipher_secret,
salt)
+ end
+
+ def set_cipher_key(cipher, key)
+ cipher.key = key
+ end
+
+ if RUBY_ENGINE == 'jruby'
+ # JRuby's OpenSSL implementation doesn't currently support passing
+ # an argument to #auth_tag. Here we work around that.
+ def auth_tag_from(cipher)
+ tag = cipher.auth_tag
+ raise Error, 'the auth tag must be 16 bytes long' if tag.bytesize
!= 16
+
+ tag
+ end
+ else
+ def auth_tag_from(cipher)
+ cipher.auth_tag(16)
+ end
+ end
end
- def verify_authenticity!(data, signature)
- raise InvalidMessage, 'Message is invalid' if data.nil? ||
signature.nil?
+ def initialize(secret, opts = {})
+ opts = opts.dup
- unless Rack::Utils.secure_compare(signature, compute_signature(data))
- raise InvalidSignature, 'HMAC is invalid'
+ @mode = opts.delete(:mode)&.to_sym || :guess_version
+ case @mode
+ when :v1
+ @v1 = V1.new(secret, opts)
+ when :v2
+ @v2 = V2.new(secret, opts)
+ else
+ @v1 = V1.new(secret, opts)
+ @v2 = V2.new(secret, opts)
end
end
- # Returns a serialized payload of the message. If a :pad_size is
supplied,
- # the message will be padded. The first 2 bytes of the returned string
will
- # indicating the amount of padding.
- def serialize_payload(message)
- serialized_data = serializer.dump(message)
+ def decrypt(base64_data)
+ decryptor =
+ case @mode
+ when :v2
+ v2
+ when :v1
+ v1
+ else
+ guess_decryptor(base64_data)
+ end
- return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
+ decryptor.decrypt(base64_data)
+ end
- padding_bytes = @options[:pad_size] - (2 + serialized_data.size) %
@options[:pad_size]
- padding_data = SecureRandom.random_bytes(padding_bytes)
+ def encrypt(message)
+ encryptor =
+ case @mode
+ when :v1
+ v1
+ else
+ v2
+ end
- "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
+ encryptor.encrypt(message)
end
- # Return the deserialized message. The first 2 bytes will be read as the
- # amount of padding.
- def deserialized_message(data)
- # Read the first 2 bytes as the padding_bytes size
- padding_bytes, = data.unpack('v')
+ private
+
+ attr_reader :v1, :v2
- # Slice out the serialized_data and deserialize it
- serialized_data = data.slice(2 + padding_bytes, data.bytesize)
- serializer.load serialized_data
+ def guess_decryptor(base64_data)
+ raise InvalidMessage, 'invalid message' if base64_data.nil? ||
base64_data.bytesize < 4
+
+ first_encoded_4_bytes = base64_data.slice(0, 4)
+ # Transform the 4 bytes into non-URL-safe base64-encoded data. Nothing
+ # happens if the data is already non-URL-safe base64.
+ first_encoded_4_bytes.tr!('-_', '+/')
+ first_decoded_3_bytes = Base64.strict_decode64(first_encoded_4_bytes)
+
+ version = first_decoded_3_bytes[0]
+ case version
+ when "\2"
+ v2
+ when "\1"
+ v1
+ else
+ raise InvalidMessage, 'invalid message'
+ end
+ rescue ArgumentError
+ raise InvalidMessage, 'invalid message'
end
end
end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/lib/rack/session/version.rb
new/lib/rack/session/version.rb
--- old/lib/rack/session/version.rb 2025-05-06 12:54:56.000000000 +0200
+++ new/lib/rack/session/version.rb 1980-01-02 01:00:00.000000000 +0100
@@ -5,6 +5,6 @@
module Rack
module Session
- VERSION = "2.1.1"
+ VERSION = "2.1.2"
end
end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/lib/rack/session.rb new/lib/rack/session.rb
--- old/lib/rack/session.rb 2025-05-06 12:54:56.000000000 +0200
+++ new/lib/rack/session.rb 1980-01-02 01:00:00.000000000 +0100
@@ -8,6 +8,5 @@
module Session
autoload :Cookie, "rack/session/cookie"
autoload :Pool, "rack/session/pool"
- autoload :Memcache, "rack/session/memcache"
end
end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/metadata new/metadata
--- old/metadata 2025-05-06 12:54:56.000000000 +0200
+++ new/metadata 1980-01-02 01:00:00.000000000 +0100
@@ -1,17 +1,16 @@
--- !ruby/object:Gem::Specification
name: rack-session
version: !ruby/object:Gem::Version
- version: 2.1.1
+ version: 2.1.2
platform: ruby
authors:
- Samuel Williams
- Jeremy Evans
- Jon Dufresne
- Philip Arndt
-autorequire:
bindir: bin
cert_chain: []
-date: 2025-05-06 00:00:00.000000000 Z
+date: 1980-01-02 00:00:00.000000000 Z
dependencies:
- !ruby/object:Gem::Dependency
name: base64
@@ -111,8 +110,6 @@
- - ">="
- !ruby/object:Gem::Version
version: '0'
-description:
-email:
executables: []
extensions: []
extra_rdoc_files: []
@@ -133,7 +130,6 @@
- MIT
metadata:
rubygems_mfa_required: 'true'
-post_install_message:
rdoc_options: []
require_paths:
- lib
@@ -148,8 +144,7 @@
- !ruby/object:Gem::Version
version: '0'
requirements: []
-rubygems_version: 3.5.22
-signing_key:
+rubygems_version: 3.6.9
specification_version: 4
summary: A session implementation for Rack.
test_files: []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/releases.md new/releases.md
--- old/releases.md 2025-05-06 12:54:56.000000000 +0200
+++ new/releases.md 1980-01-02 01:00:00.000000000 +0100
@@ -1,5 +1,9 @@
# Releases
+## v2.1.2
+
+ - [CVE-2026-39324](https://github.com/advisories/GHSA-33qg-7wpp-89cq) Don't
fall back to unencrypted coder if encryptors are present.
+
## v2.1.1
- Prevent `Rack::Session::Pool` from recreating deleted sessions
[CVE-2025-46336](https://github.com/rack/rack-session/security/advisories/GHSA-9j94-67jr-4cqj).