Skip to content

Bump rack-session from 2.1.1 to 2.1.2#908

Open
dependabot[bot] wants to merge 1 commit intomainfrom
dependabot/bundler/rack-session-2.1.2
Open

Bump rack-session from 2.1.1 to 2.1.2#908
dependabot[bot] wants to merge 1 commit intomainfrom
dependabot/bundler/rack-session-2.1.2

Conversation

@dependabot
Copy link
Copy Markdown
Contributor

@dependabot dependabot bot commented on behalf of github Apr 8, 2026

Bumps rack-session from 2.1.1 to 2.1.2.

Changelog

Sourced from rack-session's changelog.

v2.1.2

  • CVE-2026-39324 Don't fall back to unencrypted coder if encryptors are present.
Commits
  • 504367b Bump patch version.
  • f43638c Don't fall back to unencrypted coder if encryptors are present.
  • dadcfe6 Bump actions/checkout from 4 to 5 (#54)
  • 4eb9ea8 Add top level session spec to validate existing formats.
  • 8f94577 Add rails to external tests.
  • 38ea47d Allow the v2 encryptor to serialize messages with Marshal (#44)
  • 43f2e3a Fix compatibility with older Rubies.
  • 6a060b8 Support UTF-8 data when using the JSON serializer (#39)
  • 8ce0146 Fix auth_tag retrieval on JRuby (#32)
  • 7727185 Add AEAD encryption (#23)
  • See full diff in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    You can disable automated security fix PRs for this repo from the Security Alerts page.

Bumps [rack-session](https://github.com/rack/rack-session) from 2.1.1 to 2.1.2.
- [Release notes](https://github.com/rack/rack-session/releases)
- [Changelog](https://github.com/rack/rack-session/blob/main/releases.md)
- [Commits](rack/rack-session@v2.1.1...v2.1.2)

---
updated-dependencies:
- dependency-name: rack-session
  dependency-version: 2.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot bot added dependencies ruby Pull requests that update Ruby code labels Apr 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

4 similar comments
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT date:
    0.2.0: 2023-11-07 00:00:00 UTC
    0.3.0: 1980-01-02 00:00:00 UTC
  DIFFERENT metadata:
    0.2.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64"}
    0.3.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64", "changelog_uri" => "https://github.com/ruby/base64/releases"}
  DIFFERENT rubygems_version:
    0.2.0: 3.5.0.dev
    0.3.0: 3.6.7
  DIFFERENT version:
    0.2.0: 0.2.0
    0.3.0: 0.3.0
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
            BSDL +22/-0
            COPYING +56/-0
            LEGAL +60/-0
            sig/base64.rbs +355/-0
      * Changed:
            lib/base64.rb +34/-16

3 similar comments
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT date:
    0.2.0: 2023-11-07 00:00:00 UTC
    0.3.0: 1980-01-02 00:00:00 UTC
  DIFFERENT metadata:
    0.2.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64"}
    0.3.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64", "changelog_uri" => "https://github.com/ruby/base64/releases"}
  DIFFERENT rubygems_version:
    0.2.0: 3.5.0.dev
    0.3.0: 3.6.7
  DIFFERENT version:
    0.2.0: 0.2.0
    0.3.0: 0.3.0
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
            BSDL +22/-0
            COPYING +56/-0
            LEGAL +60/-0
            sig/base64.rbs +355/-0
      * Changed:
            lib/base64.rb +34/-16

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT date:
    0.2.0: 2023-11-07 00:00:00 UTC
    0.3.0: 1980-01-02 00:00:00 UTC
  DIFFERENT metadata:
    0.2.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64"}
    0.3.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64", "changelog_uri" => "https://github.com/ruby/base64/releases"}
  DIFFERENT rubygems_version:
    0.2.0: 3.5.0.dev
    0.3.0: 3.6.7
  DIFFERENT version:
    0.2.0: 0.2.0
    0.3.0: 0.3.0
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
            BSDL +22/-0
            COPYING +56/-0
            LEGAL +60/-0
            sig/base64.rbs +355/-0
      * Changed:
            lib/base64.rb +34/-16

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT date:
    0.2.0: 2023-11-07 00:00:00 UTC
    0.3.0: 1980-01-02 00:00:00 UTC
  DIFFERENT metadata:
    0.2.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64"}
    0.3.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64", "changelog_uri" => "https://github.com/ruby/base64/releases"}
  DIFFERENT rubygems_version:
    0.2.0: 3.5.0.dev
    0.3.0: 3.6.7
  DIFFERENT version:
    0.2.0: 0.2.0
    0.3.0: 0.3.0
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
            BSDL +22/-0
            COPYING +56/-0
            LEGAL +60/-0
            sig/base64.rbs +355/-0
      * Changed:
            lib/base64.rb +34/-16

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
        BSDL
                --- /tmp/20260408-322-ssonr9	2026-04-08 03:05:52.868946663 +0000
                +++ /tmp/d20260408-322-6kila7/base64-0.3.0/BSDL	2026-04-08 03:05:52.868946663 +0000
                @@ -0,0 +1,22 @@
                +Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
                +
                +Redistribution and use in source and binary forms, with or without
                +modification, are permitted provided that the following conditions
                +are met:
                +1. Redistributions of source code must retain the above copyright
                +   notice, this list of conditions and the following disclaimer.
                +2. Redistributions in binary form must reproduce the above copyright
                +   notice, this list of conditions and the following disclaimer in the
                +   documentation and/or other materials provided with the distribution.
                +
                +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
                +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
                +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +SUCH DAMAGE.
        COPYING
                --- /tmp/20260408-322-vdys5g	2026-04-08 03:05:52.871946645 +0000
                +++ /tmp/d20260408-322-6kila7/base64-0.3.0/COPYING	2026-04-08 03:05:52.868946663 +0000
                @@ -0,0 +1,56 @@
                +Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
                +You can redistribute it and/or modify it under either the terms of the
                +2-clause BSDL (see the file BSDL), or the conditions below:
                +
                +1. You may make and give away verbatim copies of the source form of the
                +   software without restriction, provided that you duplicate all of the
                +   original copyright notices and associated disclaimers.
                +
                +2. You may modify your copy of the software in any way, provided that
                +   you do at least ONE of the following:
                +
                +   a. place your modifications in the Public Domain or otherwise
                +      make them Freely Available, such as by posting said
                +      modifications to Usenet or an equivalent medium, or by allowing
                +      the author to include your modifications in the software.
                +
                +   b. use the modified software only within your corporation or
                +      organization.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +3. You may distribute the software in object code or binary form,
                +   provided that you do at least ONE of the following:
                +
                +   a. distribute the binaries and library files of the software,
                +      together with instructions (in the manual page or equivalent)
                +      on where to get the original distribution.
                +
                +   b. accompany the distribution with the machine-readable source of
                +      the software.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +4. You may modify and include the part of the software into any other
                +   software (possibly commercial).  But some files in the distribution
                +   are not written by the author, so that they are not under these terms.
                +
                +   For the list of those files and their copying conditions, see the
                +   file LEGAL.
                +
                +5. The scripts and library files supplied as input to or produced as
                +   output from the software do not automatically fall under the
                +   copyright of the software, but belong to whomever generated them,
                +   and may be sold commercially, and may be aggregated with this
                +   software.
                +
                +6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
                +   IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
                +   WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
                +   PURPOSE.
        LEGAL
                --- /tmp/20260408-322-karmau	2026-04-08 03:05:52.872946638 +0000
                +++ /tmp/d20260408-322-6kila7/base64-0.3.0/LEGAL	2026-04-08 03:05:52.868946663 +0000
                @@ -0,0 +1,60 @@
                +# -*- rdoc -*-
                +
                += LEGAL NOTICE INFORMATION
                +--------------------------
                +
                +All the files in this distribution are covered under either the Ruby's
                +license (see the file COPYING) or public-domain except some files
                +mentioned below.
                +
                +== MIT License
                +>>>
                +      Permission is hereby granted, free of charge, to any person obtaining
                +      a copy of this software and associated documentation files (the
                +      "Software"), to deal in the Software without restriction, including
                +      without limitation the rights to use, copy, modify, merge, publish,
                +      distribute, sublicense, and/or sell copies of the Software, and to
                +      permit persons to whom the Software is furnished to do so, subject to
                +      the following conditions:
                +
                +      The above copyright notice and this permission notice shall be
                +      included in all copies or substantial portions of the Software.
                +
                +      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
                +      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
                +      MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
                +      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
                +      LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
                +      OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
                +      WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
                +
                +== Old-style BSD license
                +>>>
                +      Redistribution and use in source and binary forms, with or without
                +      modification, are permitted provided that the following conditions
                +      are met:
                +      1. Redistributions of source code must retain the above copyright
                +         notice, this list of conditions and the following disclaimer.
                +      2. Redistributions in binary form must reproduce the above copyright
                +         notice, this list of conditions and the following disclaimer in the
                +         documentation and/or other materials provided with the distribution.
                +      3. Neither the name of the University nor the names of its contributors
                +         may be used to endorse or promote products derived from this software
                +         without specific prior written permission.
                +
                +      THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
                +      ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +      IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +      ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
                +      FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +      DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +      OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +      HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +      LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +      OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +      SUCH DAMAGE.
                +
                +      IMPORTANT NOTE::
                +
                +      From ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change
                +      paragraph 3 above is now null and void.
        sig/base64.rbs
                --- /tmp/20260408-322-55osh	2026-04-08 03:05:52.872946638 +0000
                +++ /tmp/d20260408-322-6kila7/base64-0.3.0/sig/base64.rbs	2026-04-08 03:05:52.868946663 +0000
                @@ -0,0 +1,355 @@
                +# <!-- rdoc-file=lib/base64.rb -->
                +# Module Base64 provides methods for:
                +#
                +# *   Encoding a binary string (containing non-ASCII characters) as a string of
                +#     printable ASCII characters.
                +# *   Decoding such an encoded string.
                +#
                +# Base64 is commonly used in contexts where binary data is not allowed or
                +# supported:
                +#
                +# *   Images in HTML or CSS files, or in URLs.
                +# *   Email attachments.
                +#
                +# A Base64-encoded string is about one-third larger that its source. See the
                +# [Wikipedia article](https://en.wikipedia.org/wiki/Base64) for more
                +# information.
                +#
                +# This module provides three pairs of encode/decode methods. Your choices among
                +# these methods should depend on:
                +#
                +# *   Which character set is to be used for encoding and decoding.
                +# *   Whether "padding" is to be used.
                +# *   Whether encoded strings are to contain newlines.
                +#
                +# Note: Examples on this page assume that the including program has executed:
                +#
                +#     require 'base64'
                +#
                +# ## Encoding Character Sets
                +#
                +# A Base64-encoded string consists only of characters from a 64-character set:
                +#
                +# *   `('A'..'Z')`.
                +# *   `('a'..'z')`.
                +# *   `('0'..'9')`.
                +# *   `=`, the 'padding' character.
                +# *   Either:
                +#     *   `%w[+ /]`:
                +#         [RFC-2045-compliant](https://datatracker.ietf.org/doc/html/rfc2045);
                +#         *not* safe for URLs.
                +#     *   `%w[- _]`:
                +#         [RFC-4648-compliant](https://datatracker.ietf.org/doc/html/rfc4648);
                +#         safe for URLs.
                +#
                +# If you are working with Base64-encoded strings that will come from or be put
                +# into URLs, you should choose this encoder-decoder pair of RFC-4648-compliant
                +# methods:
                +#
                +# *   Base64.urlsafe_encode64 and Base64.urlsafe_decode64.
                +#
                +# Otherwise, you may choose any of the pairs in this module, including the pair
                +# above, or the RFC-2045-compliant pairs:
                +#
                +# *   Base64.encode64 and Base64.decode64.
                +# *   Base64.strict_encode64 and Base64.strict_decode64.
                +#
                +# ## Padding
                +#
                +# Base64-encoding changes a triplet of input bytes into a quartet of output
                +# characters.
                +#
                +# **Padding in Encode Methods**
                +#
                +# Padding -- extending an encoded string with zero, one, or two trailing `=`
                +# characters -- is performed by methods Base64.encode64, Base64.strict_encode64,
                +# and, by default, Base64.urlsafe_encode64:
                +#
                +#     Base64.encode64('s')                         # => "cw==\n"
                +#     Base64.strict_encode64('s')                  # => "cw=="
                +#     Base64.urlsafe_encode64('s')                 # => "cw=="
                +#     Base64.urlsafe_encode64('s', padding: false) # => "cw"
                +#
                +# When padding is performed, the encoded string is always of length *4n*, where
                +# `n` is a non-negative integer:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.strict_encode64('123')      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.strict_encode64('123456')   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate padded output characters of length
                +#     *4(n+1)*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 8 characters.
                +#         Base64.strict_encode64('1234')     # => "MDEyMw=="
                +#         # n = 2:  7 bytes => 12 characters.
                +#         Base64.strict_encode64('1234567')  # => "MDEyMzQ1Ng=="
                +#
                +# *   Input bytes of length *3n+2* generate padded output characters of length
                +#     *4(n+1)*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 8 characters.
                +#         Base64.strict_encode64('12345')    # => "MDEyMzQ="
                +#         # n = 2:  8 bytes => 12 characters.
                +#         Base64.strict_encode64('12345678') # => "MDEyMzQ1Njc="
                +#
                +# When padding is suppressed, for a positive integer *n*:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.urlsafe_encode64('123', padding: false)      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.urlsafe_encode64('123456', padding: false)   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate unpadded output characters of length
                +#     *4n+2*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 6 characters.
                +#         Base64.urlsafe_encode64('1234', padding: false)     # => "MDEyMw"
                +#         # n = 2:  7 bytes => 10 characters.
                +#         Base64.urlsafe_encode64('1234567', padding: false)  # => "MDEyMzQ1Ng"
                +#
                +# *   Input bytes of length *3n+2* generate unpadded output characters of length
                +#     *4n+3*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 7 characters.
                +#         Base64.urlsafe_encode64('12345', padding: false)    # => "MDEyMzQ"
                +#         # m = 2:  8 bytes => 11 characters.
                +#         Base64.urlsafe_encode64('12345678', padding: false) # => "MDEyMzQ1Njc"
                +#
                +# **Padding in Decode Methods**
                +#
                +# All of the Base64 decode methods support (but do not require) padding.
                +#
                +# Method Base64.decode64 does not check the size of the padding:
                +#
                +#     Base64.decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +#
                +# Method Base64.strict_decode64 strictly enforces padding size:
                +#
                +#     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +#     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +#     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +#
                +# Method Base64.urlsafe_decode64 allows padding in `str`, which if present, must
                +# be correct: see [Padding](Base64.html#module-Base64-label-Padding), above:
                +#
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +#
                +# ## Newlines
                +#
                +# An encoded string returned by Base64.encode64 or Base64.urlsafe_encode64 has
                +# an embedded newline character after each 60-character sequence, and, if
                +# non-empty, at the end:
                +#
                +#     # No newline if empty.
                +#     encoded = Base64.encode64("\x00" *  0)
                +#     encoded.index("\n") # => nil
                +#
                +#     # Newline at end of short output.
                +#     encoded = Base64.encode64("\x00" *  1)
                +#     encoded.size        # => 4
                +#     encoded.index("\n") # => 4
                +#
                +#     # Newline at end of longer output.
                +#     encoded = Base64.encode64("\x00" * 45)
                +#     encoded.size        # => 60
                +#     encoded.index("\n") # => 60
                +#
                +#     # Newlines embedded and at end of still longer output.
                +#     encoded = Base64.encode64("\x00" * 46)
                +#     encoded.size                          # => 65
                +#     encoded.rindex("\n")                  # => 65
                +#     encoded.split("\n").map {|s| s.size } # => [60, 4]
                +#
                +# The string to be encoded may itself contain newlines, which are encoded as
                +# Base64:
                +#
                +#       #   Base64.encode64("\n\n\n") # => "CgoK\n"
                +#     s = "This is line 1\nThis is line 2\n"
                +#     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +#
                +module Base64
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #     Base64.decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` are ignored; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.decode64("\x00\n-_") # => ""
                +  #
                +  # Padding in `str` (even if incorrect) is ignored:
                +  #
                +  #     Base64.decode64("MDEyMzQ1Njc")   # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +  #
                +  def self?.decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string ends with a newline character, and if sufficiently long
                +  # will have one or more embedded newline characters; see
                +  # [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #     Base64.encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq\nKg==\n"
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.encode64("\n\n\n") # => "CgoK\n"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #
                +  def self?.encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #     Base64.strict_decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` not allowed; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.strict_decode64("\n") # Raises ArgumentError
                +  #     Base64.strict_decode64('-')  # Raises ArgumentError
                +  #     Base64.strict_decode64('_')  # Raises ArgumentError
                +  #
                +  # Padding in `str`, if present, must be correct:
                +  #
                +  #     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +  #     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +  #
                +  def self?.strict_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.strict_encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.strict_encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg=="
                +  #     Base64.strict_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.strict_encode64("\n\n\n") # => "CgoK"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.strict_encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #
                +  def self?.strict_encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_decode64(str)
                +  # -->
                +  # Returns the decoding of an RFC-4648-compliant Base64-encoded string `str`:
                +  #
                +  # `str` may not contain non-Base64 characters; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_decode64('+')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64('/')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64("\n") # Raises ArgumentError.
                +  #
                +  # Padding in `str`, if present, must be correct: see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +  #
                +  def self?.urlsafe_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_encode64(bin, padding: true)
                +  # -->
                +  # Returns the RFC-4648-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 4648, the returned string will not contain the URL-unsafe characters
                +  # `+` or `/`, but instead may contain the URL-safe characters `-` and `_`; see
                +  # [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_encode64("\xFB\xEF\xBE") # => "----"
                +  #     Base64.urlsafe_encode64("\xFF\xFF\xFF") # => "____"
                +  #
                +  # By default, the returned string may have padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #
                +  # Optionally, you can suppress padding:
                +  #
                +  #     Base64.urlsafe_encode64('*', padding: false) # => "Kg"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #     Base64.urlsafe_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  def self?.urlsafe_encode64: (String bin, ?padding: boolish) -> String
                +end
      * Changed:
        lib/base64.rb
                --- /tmp/d20260408-322-6kila7/base64-0.2.0/lib/base64.rb	2026-04-08 03:05:52.867946670 +0000
                +++ /tmp/d20260408-322-6kila7/base64-0.3.0/lib/base64.rb	2026-04-08 03:05:52.868946663 +0000
                @@ -5 +5 @@
                -# - Encoding a binary string (containing non-ASCII characters)
                +# - \Encoding a binary string (containing non-ASCII characters)
                @@ -30 +30 @@
                -# == Encoding Character Sets
                +# == \Encoding Character Sets
                @@ -143 +143 @@
                -# \Method Base64.urlsafe_decode64 allows padding in +str+,
                +# \Method Base64.urlsafe_decode64 allows padding in the encoded string,
                @@ -186 +186 @@
                -  VERSION = "0.2.0"
                +  VERSION = "0.3.0"
                @@ -190 +190,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -222,0 +226,3 @@
                +  # :call-seq:
                +  #   Base64.decode(encoded_string) -> decoded_string
                +  #
                @@ -224 +230 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -229 +235 @@
                -  # Non-\Base64 characters in +str+ are ignored;
                +  # Non-\Base64 characters in +encoded_string+ are ignored;
                @@ -235 +241 @@
                -  # Padding in +str+ (even if incorrect) is ignored:
                +  # Padding in +encoded_string+ (even if incorrect) is ignored:
                @@ -245 +251,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.strict_encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -276,0 +286,3 @@
                +  # :call-seq:
                +  #   Base64.strict_decode64(encoded_string) -> decoded_string
                +  #
                @@ -278 +290 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -283 +295 @@
                -  # Non-\Base64 characters in +str+ not allowed;
                +  # Non-\Base64 characters in +encoded_string+ are not allowed;
                @@ -291 +303 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:
                @@ -301 +313,4 @@
                -  # Returns the RFC-4648-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.urlsafe_encode64(string) -> encoded_string
                +  #
                +  # Returns the RFC-4648-compliant \Base64-encoding of +string+.
                @@ -335 +350,4 @@
                -  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +str+:
                +  # :call-seq:
                +  #   Base64.urlsafe_decode64(encoded_string) -> decoded_string
                +  #
                +  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +encoded_string+:
                @@ -337 +355 @@
                -  # +str+ may not contain non-Base64 characters;
                +  # +encoded_string+ may not contain non-Base64 characters;
                @@ -344 +362 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
        BSDL
                --- /tmp/20260408-324-iferh2	2026-04-08 03:05:56.095365456 +0000
                +++ /tmp/d20260408-324-lgaro/base64-0.3.0/BSDL	2026-04-08 03:05:56.094365458 +0000
                @@ -0,0 +1,22 @@
                +Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
                +
                +Redistribution and use in source and binary forms, with or without
                +modification, are permitted provided that the following conditions
                +are met:
                +1. Redistributions of source code must retain the above copyright
                +   notice, this list of conditions and the following disclaimer.
                +2. Redistributions in binary form must reproduce the above copyright
                +   notice, this list of conditions and the following disclaimer in the
                +   documentation and/or other materials provided with the distribution.
                +
                +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
                +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
                +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +SUCH DAMAGE.
        COPYING
                --- /tmp/20260408-324-aa7shn	2026-04-08 03:05:56.097365453 +0000
                +++ /tmp/d20260408-324-lgaro/base64-0.3.0/COPYING	2026-04-08 03:05:56.094365458 +0000
                @@ -0,0 +1,56 @@
                +Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
                +You can redistribute it and/or modify it under either the terms of the
                +2-clause BSDL (see the file BSDL), or the conditions below:
                +
                +1. You may make and give away verbatim copies of the source form of the
                +   software without restriction, provided that you duplicate all of the
                +   original copyright notices and associated disclaimers.
                +
                +2. You may modify your copy of the software in any way, provided that
                +   you do at least ONE of the following:
                +
                +   a. place your modifications in the Public Domain or otherwise
                +      make them Freely Available, such as by posting said
                +      modifications to Usenet or an equivalent medium, or by allowing
                +      the author to include your modifications in the software.
                +
                +   b. use the modified software only within your corporation or
                +      organization.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +3. You may distribute the software in object code or binary form,
                +   provided that you do at least ONE of the following:
                +
                +   a. distribute the binaries and library files of the software,
                +      together with instructions (in the manual page or equivalent)
                +      on where to get the original distribution.
                +
                +   b. accompany the distribution with the machine-readable source of
                +      the software.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +4. You may modify and include the part of the software into any other
                +   software (possibly commercial).  But some files in the distribution
                +   are not written by the author, so that they are not under these terms.
                +
                +   For the list of those files and their copying conditions, see the
                +   file LEGAL.
                +
                +5. The scripts and library files supplied as input to or produced as
                +   output from the software do not automatically fall under the
                +   copyright of the software, but belong to whomever generated them,
                +   and may be sold commercially, and may be aggregated with this
                +   software.
                +
                +6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
                +   IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
                +   WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
                +   PURPOSE.
        LEGAL
                --- /tmp/20260408-324-ap33xy	2026-04-08 03:05:56.098365452 +0000
                +++ /tmp/d20260408-324-lgaro/base64-0.3.0/LEGAL	2026-04-08 03:05:56.094365458 +0000
                @@ -0,0 +1,60 @@
                +# -*- rdoc -*-
                +
                += LEGAL NOTICE INFORMATION
                +--------------------------
                +
                +All the files in this distribution are covered under either the Ruby's
                +license (see the file COPYING) or public-domain except some files
                +mentioned below.
                +
                +== MIT License
                +>>>
                +      Permission is hereby granted, free of charge, to any person obtaining
                +      a copy of this software and associated documentation files (the
                +      "Software"), to deal in the Software without restriction, including
                +      without limitation the rights to use, copy, modify, merge, publish,
                +      distribute, sublicense, and/or sell copies of the Software, and to
                +      permit persons to whom the Software is furnished to do so, subject to
                +      the following conditions:
                +
                +      The above copyright notice and this permission notice shall be
                +      included in all copies or substantial portions of the Software.
                +
                +      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
                +      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
                +      MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
                +      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
                +      LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
                +      OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
                +      WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
                +
                +== Old-style BSD license
                +>>>
                +      Redistribution and use in source and binary forms, with or without
                +      modification, are permitted provided that the following conditions
                +      are met:
                +      1. Redistributions of source code must retain the above copyright
                +         notice, this list of conditions and the following disclaimer.
                +      2. Redistributions in binary form must reproduce the above copyright
                +         notice, this list of conditions and the following disclaimer in the
                +         documentation and/or other materials provided with the distribution.
                +      3. Neither the name of the University nor the names of its contributors
                +         may be used to endorse or promote products derived from this software
                +         without specific prior written permission.
                +
                +      THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
                +      ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +      IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +      ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
                +      FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +      DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +      OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +      HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +      LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +      OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +      SUCH DAMAGE.
                +
                +      IMPORTANT NOTE::
                +
                +      From ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change
                +      paragraph 3 above is now null and void.
        sig/base64.rbs
                --- /tmp/20260408-324-5le0bz	2026-04-08 03:05:56.099365450 +0000
                +++ /tmp/d20260408-324-lgaro/base64-0.3.0/sig/base64.rbs	2026-04-08 03:05:56.095365456 +0000
                @@ -0,0 +1,355 @@
                +# <!-- rdoc-file=lib/base64.rb -->
                +# Module Base64 provides methods for:
                +#
                +# *   Encoding a binary string (containing non-ASCII characters) as a string of
                +#     printable ASCII characters.
                +# *   Decoding such an encoded string.
                +#
                +# Base64 is commonly used in contexts where binary data is not allowed or
                +# supported:
                +#
                +# *   Images in HTML or CSS files, or in URLs.
                +# *   Email attachments.
                +#
                +# A Base64-encoded string is about one-third larger that its source. See the
                +# [Wikipedia article](https://en.wikipedia.org/wiki/Base64) for more
                +# information.
                +#
                +# This module provides three pairs of encode/decode methods. Your choices among
                +# these methods should depend on:
                +#
                +# *   Which character set is to be used for encoding and decoding.
                +# *   Whether "padding" is to be used.
                +# *   Whether encoded strings are to contain newlines.
                +#
                +# Note: Examples on this page assume that the including program has executed:
                +#
                +#     require 'base64'
                +#
                +# ## Encoding Character Sets
                +#
                +# A Base64-encoded string consists only of characters from a 64-character set:
                +#
                +# *   `('A'..'Z')`.
                +# *   `('a'..'z')`.
                +# *   `('0'..'9')`.
                +# *   `=`, the 'padding' character.
                +# *   Either:
                +#     *   `%w[+ /]`:
                +#         [RFC-2045-compliant](https://datatracker.ietf.org/doc/html/rfc2045);
                +#         *not* safe for URLs.
                +#     *   `%w[- _]`:
                +#         [RFC-4648-compliant](https://datatracker.ietf.org/doc/html/rfc4648);
                +#         safe for URLs.
                +#
                +# If you are working with Base64-encoded strings that will come from or be put
                +# into URLs, you should choose this encoder-decoder pair of RFC-4648-compliant
                +# methods:
                +#
                +# *   Base64.urlsafe_encode64 and Base64.urlsafe_decode64.
                +#
                +# Otherwise, you may choose any of the pairs in this module, including the pair
                +# above, or the RFC-2045-compliant pairs:
                +#
                +# *   Base64.encode64 and Base64.decode64.
                +# *   Base64.strict_encode64 and Base64.strict_decode64.
                +#
                +# ## Padding
                +#
                +# Base64-encoding changes a triplet of input bytes into a quartet of output
                +# characters.
                +#
                +# **Padding in Encode Methods**
                +#
                +# Padding -- extending an encoded string with zero, one, or two trailing `=`
                +# characters -- is performed by methods Base64.encode64, Base64.strict_encode64,
                +# and, by default, Base64.urlsafe_encode64:
                +#
                +#     Base64.encode64('s')                         # => "cw==\n"
                +#     Base64.strict_encode64('s')                  # => "cw=="
                +#     Base64.urlsafe_encode64('s')                 # => "cw=="
                +#     Base64.urlsafe_encode64('s', padding: false) # => "cw"
                +#
                +# When padding is performed, the encoded string is always of length *4n*, where
                +# `n` is a non-negative integer:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.strict_encode64('123')      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.strict_encode64('123456')   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate padded output characters of length
                +#     *4(n+1)*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 8 characters.
                +#         Base64.strict_encode64('1234')     # => "MDEyMw=="
                +#         # n = 2:  7 bytes => 12 characters.
                +#         Base64.strict_encode64('1234567')  # => "MDEyMzQ1Ng=="
                +#
                +# *   Input bytes of length *3n+2* generate padded output characters of length
                +#     *4(n+1)*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 8 characters.
                +#         Base64.strict_encode64('12345')    # => "MDEyMzQ="
                +#         # n = 2:  8 bytes => 12 characters.
                +#         Base64.strict_encode64('12345678') # => "MDEyMzQ1Njc="
                +#
                +# When padding is suppressed, for a positive integer *n*:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.urlsafe_encode64('123', padding: false)      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.urlsafe_encode64('123456', padding: false)   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate unpadded output characters of length
                +#     *4n+2*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 6 characters.
                +#         Base64.urlsafe_encode64('1234', padding: false)     # => "MDEyMw"
                +#         # n = 2:  7 bytes => 10 characters.
                +#         Base64.urlsafe_encode64('1234567', padding: false)  # => "MDEyMzQ1Ng"
                +#
                +# *   Input bytes of length *3n+2* generate unpadded output characters of length
                +#     *4n+3*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 7 characters.
                +#         Base64.urlsafe_encode64('12345', padding: false)    # => "MDEyMzQ"
                +#         # m = 2:  8 bytes => 11 characters.
                +#         Base64.urlsafe_encode64('12345678', padding: false) # => "MDEyMzQ1Njc"
                +#
                +# **Padding in Decode Methods**
                +#
                +# All of the Base64 decode methods support (but do not require) padding.
                +#
                +# Method Base64.decode64 does not check the size of the padding:
                +#
                +#     Base64.decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +#
                +# Method Base64.strict_decode64 strictly enforces padding size:
                +#
                +#     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +#     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +#     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +#
                +# Method Base64.urlsafe_decode64 allows padding in `str`, which if present, must
                +# be correct: see [Padding](Base64.html#module-Base64-label-Padding), above:
                +#
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +#
                +# ## Newlines
                +#
                +# An encoded string returned by Base64.encode64 or Base64.urlsafe_encode64 has
                +# an embedded newline character after each 60-character sequence, and, if
                +# non-empty, at the end:
                +#
                +#     # No newline if empty.
                +#     encoded = Base64.encode64("\x00" *  0)
                +#     encoded.index("\n") # => nil
                +#
                +#     # Newline at end of short output.
                +#     encoded = Base64.encode64("\x00" *  1)
                +#     encoded.size        # => 4
                +#     encoded.index("\n") # => 4
                +#
                +#     # Newline at end of longer output.
                +#     encoded = Base64.encode64("\x00" * 45)
                +#     encoded.size        # => 60
                +#     encoded.index("\n") # => 60
                +#
                +#     # Newlines embedded and at end of still longer output.
                +#     encoded = Base64.encode64("\x00" * 46)
                +#     encoded.size                          # => 65
                +#     encoded.rindex("\n")                  # => 65
                +#     encoded.split("\n").map {|s| s.size } # => [60, 4]
                +#
                +# The string to be encoded may itself contain newlines, which are encoded as
                +# Base64:
                +#
                +#       #   Base64.encode64("\n\n\n") # => "CgoK\n"
                +#     s = "This is line 1\nThis is line 2\n"
                +#     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +#
                +module Base64
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #     Base64.decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` are ignored; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.decode64("\x00\n-_") # => ""
                +  #
                +  # Padding in `str` (even if incorrect) is ignored:
                +  #
                +  #     Base64.decode64("MDEyMzQ1Njc")   # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +  #
                +  def self?.decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string ends with a newline character, and if sufficiently long
                +  # will have one or more embedded newline characters; see
                +  # [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #     Base64.encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq\nKg==\n"
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.encode64("\n\n\n") # => "CgoK\n"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #
                +  def self?.encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #     Base64.strict_decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` not allowed; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.strict_decode64("\n") # Raises ArgumentError
                +  #     Base64.strict_decode64('-')  # Raises ArgumentError
                +  #     Base64.strict_decode64('_')  # Raises ArgumentError
                +  #
                +  # Padding in `str`, if present, must be correct:
                +  #
                +  #     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +  #     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +  #
                +  def self?.strict_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.strict_encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.strict_encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg=="
                +  #     Base64.strict_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.strict_encode64("\n\n\n") # => "CgoK"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.strict_encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #
                +  def self?.strict_encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_decode64(str)
                +  # -->
                +  # Returns the decoding of an RFC-4648-compliant Base64-encoded string `str`:
                +  #
                +  # `str` may not contain non-Base64 characters; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_decode64('+')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64('/')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64("\n") # Raises ArgumentError.
                +  #
                +  # Padding in `str`, if present, must be correct: see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +  #
                +  def self?.urlsafe_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_encode64(bin, padding: true)
                +  # -->
                +  # Returns the RFC-4648-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 4648, the returned string will not contain the URL-unsafe characters
                +  # `+` or `/`, but instead may contain the URL-safe characters `-` and `_`; see
                +  # [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_encode64("\xFB\xEF\xBE") # => "----"
                +  #     Base64.urlsafe_encode64("\xFF\xFF\xFF") # => "____"
                +  #
                +  # By default, the returned string may have padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #
                +  # Optionally, you can suppress padding:
                +  #
                +  #     Base64.urlsafe_encode64('*', padding: false) # => "Kg"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #     Base64.urlsafe_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  def self?.urlsafe_encode64: (String bin, ?padding: boolish) -> String
                +end
      * Changed:
        lib/base64.rb
                --- /tmp/d20260408-324-lgaro/base64-0.2.0/lib/base64.rb	2026-04-08 03:05:56.094365458 +0000
                +++ /tmp/d20260408-324-lgaro/base64-0.3.0/lib/base64.rb	2026-04-08 03:05:56.095365456 +0000
                @@ -5 +5 @@
                -# - Encoding a binary string (containing non-ASCII characters)
                +# - \Encoding a binary string (containing non-ASCII characters)
                @@ -30 +30 @@
                -# == Encoding Character Sets
                +# == \Encoding Character Sets
                @@ -143 +143 @@
                -# \Method Base64.urlsafe_decode64 allows padding in +str+,
                +# \Method Base64.urlsafe_decode64 allows padding in the encoded string,
                @@ -186 +186 @@
                -  VERSION = "0.2.0"
                +  VERSION = "0.3.0"
                @@ -190 +190,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -222,0 +226,3 @@
                +  # :call-seq:
                +  #   Base64.decode(encoded_string) -> decoded_string
                +  #
                @@ -224 +230 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -229 +235 @@
                -  # Non-\Base64 characters in +str+ are ignored;
                +  # Non-\Base64 characters in +encoded_string+ are ignored;
                @@ -235 +241 @@
                -  # Padding in +str+ (even if incorrect) is ignored:
                +  # Padding in +encoded_string+ (even if incorrect) is ignored:
                @@ -245 +251,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.strict_encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -276,0 +286,3 @@
                +  # :call-seq:
                +  #   Base64.strict_decode64(encoded_string) -> decoded_string
                +  #
                @@ -278 +290 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -283 +295 @@
                -  # Non-\Base64 characters in +str+ not allowed;
                +  # Non-\Base64 characters in +encoded_string+ are not allowed;
                @@ -291 +303 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:
                @@ -301 +313,4 @@
                -  # Returns the RFC-4648-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.urlsafe_encode64(string) -> encoded_string
                +  #
                +  # Returns the RFC-4648-compliant \Base64-encoding of +string+.
                @@ -335 +350,4 @@
                -  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +str+:
                +  # :call-seq:
                +  #   Base64.urlsafe_decode64(encoded_string) -> decoded_string
                +  #
                +  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +encoded_string+:
                @@ -337 +355 @@
                -  # +str+ may not contain non-Base64 characters;
                +  # +encoded_string+ may not contain non-Base64 characters;
                @@ -344 +362 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
        BSDL
                --- /tmp/20260408-320-38ad9n	2026-04-08 03:05:58.763564553 +0000
                +++ /tmp/d20260408-320-fnp517/base64-0.3.0/BSDL	2026-04-08 03:05:58.762564558 +0000
                @@ -0,0 +1,22 @@
                +Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
                +
                +Redistribution and use in source and binary forms, with or without
                +modification, are permitted provided that the following conditions
                +are met:
                +1. Redistributions of source code must retain the above copyright
                +   notice, this list of conditions and the following disclaimer.
                +2. Redistributions in binary form must reproduce the above copyright
                +   notice, this list of conditions and the following disclaimer in the
                +   documentation and/or other materials provided with the distribution.
                +
                +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
                +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
                +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +SUCH DAMAGE.
        COPYING
                --- /tmp/20260408-320-ovwxhm	2026-04-08 03:05:58.765564542 +0000
                +++ /tmp/d20260408-320-fnp517/base64-0.3.0/COPYING	2026-04-08 03:05:58.763564553 +0000
                @@ -0,0 +1,56 @@
                +Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
                +You can redistribute it and/or modify it under either the terms of the
                +2-clause BSDL (see the file BSDL), or the conditions below:
                +
                +1. You may make and give away verbatim copies of the source form of the
                +   software without restriction, provided that you duplicate all of the
                +   original copyright notices and associated disclaimers.
                +
                +2. You may modify your copy of the software in any way, provided that
                +   you do at least ONE of the following:
                +
                +   a. place your modifications in the Public Domain or otherwise
                +      make them Freely Available, such as by posting said
                +      modifications to Usenet or an equivalent medium, or by allowing
                +      the author to include your modifications in the software.
                +
                +   b. use the modified software only within your corporation or
                +      organization.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +3. You may distribute the software in object code or binary form,
                +   provided that you do at least ONE of the following:
                +
                +   a. distribute the binaries and library files of the software,
                +      together with instructions (in the manual page or equivalent)
                +      on where to get the original distribution.
                +
                +   b. accompany the distribution with the machine-readable source of
                +      the software.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +4. You may modify and include the part of the software into any other
                +   software (possibly commercial).  But some files in the distribution
                +   are not written by the author, so that they are not under these terms.
                +
                +   For the list of those files and their copying conditions, see the
                +   file LEGAL.
                +
                +5. The scripts and library files supplied as input to or produced as
                +   output from the software do not automatically fall under the
                +   copyright of the software, but belong to whomever generated them,
                +   and may be sold commercially, and may be aggregated with this
                +   software.
                +
                +6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
                +   IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
                +   WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
                +   PURPOSE.
        LEGAL
                --- /tmp/20260408-320-x7yg6f	2026-04-08 03:05:58.767564531 +0000
                +++ /tmp/d20260408-320-fnp517/base64-0.3.0/LEGAL	2026-04-08 03:05:58.763564553 +0000
                @@ -0,0 +1,60 @@
                +# -*- rdoc -*-
                +
                += LEGAL NOTICE INFORMATION
                +--------------------------
                +
                +All the files in this distribution are covered under either the Ruby's
                +license (see the file COPYING) or public-domain except some files
                +mentioned below.
                +
                +== MIT License
                +>>>
                +      Permission is hereby granted, free of charge, to any person obtaining
                +      a copy of this software and associated documentation files (the
                +      "Software"), to deal in the Software without restriction, including
                +      without limitation the rights to use, copy, modify, merge, publish,
                +      distribute, sublicense, and/or sell copies of the Software, and to
                +      permit persons to whom the Software is furnished to do so, subject to
                +      the following conditions:
                +
                +      The above copyright notice and this permission notice shall be
                +      included in all copies or substantial portions of the Software.
                +
                +      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
                +      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
                +      MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
                +      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
                +      LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
                +      OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
                +      WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
                +
                +== Old-style BSD license
                +>>>
                +      Redistribution and use in source and binary forms, with or without
                +      modification, are permitted provided that the following conditions
                +      are met:
                +      1. Redistributions of source code must retain the above copyright
                +         notice, this list of conditions and the following disclaimer.
                +      2. Redistributions in binary form must reproduce the above copyright
                +         notice, this list of conditions and the following disclaimer in the
                +         documentation and/or other materials provided with the distribution.
                +      3. Neither the name of the University nor the names of its contributors
                +         may be used to endorse or promote products derived from this software
                +         without specific prior written permission.
                +
                +      THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
                +      ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +      IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +      ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
                +      FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +      DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +      OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +      HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +      LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +      OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +      SUCH DAMAGE.
                +
                +      IMPORTANT NOTE::
                +
                +      From ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change
                +      paragraph 3 above is now null and void.
        sig/base64.rbs
                --- /tmp/20260408-320-nujfcg	2026-04-08 03:05:58.768564526 +0000
                +++ /tmp/d20260408-320-fnp517/base64-0.3.0/sig/base64.rbs	2026-04-08 03:05:58.763564553 +0000
                @@ -0,0 +1,355 @@
                +# <!-- rdoc-file=lib/base64.rb -->
                +# Module Base64 provides methods for:
                +#
                +# *   Encoding a binary string (containing non-ASCII characters) as a string of
                +#     printable ASCII characters.
                +# *   Decoding such an encoded string.
                +#
                +# Base64 is commonly used in contexts where binary data is not allowed or
                +# supported:
                +#
                +# *   Images in HTML or CSS files, or in URLs.
                +# *   Email attachments.
                +#
                +# A Base64-encoded string is about one-third larger that its source. See the
                +# [Wikipedia article](https://en.wikipedia.org/wiki/Base64) for more
                +# information.
                +#
                +# This module provides three pairs of encode/decode methods. Your choices among
                +# these methods should depend on:
                +#
                +# *   Which character set is to be used for encoding and decoding.
                +# *   Whether "padding" is to be used.
                +# *   Whether encoded strings are to contain newlines.
                +#
                +# Note: Examples on this page assume that the including program has executed:
                +#
                +#     require 'base64'
                +#
                +# ## Encoding Character Sets
                +#
                +# A Base64-encoded string consists only of characters from a 64-character set:
                +#
                +# *   `('A'..'Z')`.
                +# *   `('a'..'z')`.
                +# *   `('0'..'9')`.
                +# *   `=`, the 'padding' character.
                +# *   Either:
                +#     *   `%w[+ /]`:
                +#         [RFC-2045-compliant](https://datatracker.ietf.org/doc/html/rfc2045);
                +#         *not* safe for URLs.
                +#     *   `%w[- _]`:
                +#         [RFC-4648-compliant](https://datatracker.ietf.org/doc/html/rfc4648);
                +#         safe for URLs.
                +#
                +# If you are working with Base64-encoded strings that will come from or be put
                +# into URLs, you should choose this encoder-decoder pair of RFC-4648-compliant
                +# methods:
                +#
                +# *   Base64.urlsafe_encode64 and Base64.urlsafe_decode64.
                +#
                +# Otherwise, you may choose any of the pairs in this module, including the pair
                +# above, or the RFC-2045-compliant pairs:
                +#
                +# *   Base64.encode64 and Base64.decode64.
                +# *   Base64.strict_encode64 and Base64.strict_decode64.
                +#
                +# ## Padding
                +#
                +# Base64-encoding changes a triplet of input bytes into a quartet of output
                +# characters.
                +#
                +# **Padding in Encode Methods**
                +#
                +# Padding -- extending an encoded string with zero, one, or two trailing `=`
                +# characters -- is performed by methods Base64.encode64, Base64.strict_encode64,
                +# and, by default, Base64.urlsafe_encode64:
                +#
                +#     Base64.encode64('s')                         # => "cw==\n"
                +#     Base64.strict_encode64('s')                  # => "cw=="
                +#     Base64.urlsafe_encode64('s')                 # => "cw=="
                +#     Base64.urlsafe_encode64('s', padding: false) # => "cw"
                +#
                +# When padding is performed, the encoded string is always of length *4n*, where
                +# `n` is a non-negative integer:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.strict_encode64('123')      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.strict_encode64('123456')   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate padded output characters of length
                +#     *4(n+1)*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 8 characters.
                +#         Base64.strict_encode64('1234')     # => "MDEyMw=="
                +#         # n = 2:  7 bytes => 12 characters.
                +#         Base64.strict_encode64('1234567')  # => "MDEyMzQ1Ng=="
                +#
                +# *   Input bytes of length *3n+2* generate padded output characters of length
                +#     *4(n+1)*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 8 characters.
                +#         Base64.strict_encode64('12345')    # => "MDEyMzQ="
                +#         # n = 2:  8 bytes => 12 characters.
                +#         Base64.strict_encode64('12345678') # => "MDEyMzQ1Njc="
                +#
                +# When padding is suppressed, for a positive integer *n*:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.urlsafe_encode64('123', padding: false)      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.urlsafe_encode64('123456', padding: false)   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate unpadded output characters of length
                +#     *4n+2*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 6 characters.
                +#         Base64.urlsafe_encode64('1234', padding: false)     # => "MDEyMw"
                +#         # n = 2:  7 bytes => 10 characters.
                +#         Base64.urlsafe_encode64('1234567', padding: false)  # => "MDEyMzQ1Ng"
                +#
                +# *   Input bytes of length *3n+2* generate unpadded output characters of length
                +#     *4n+3*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 7 characters.
                +#         Base64.urlsafe_encode64('12345', padding: false)    # => "MDEyMzQ"
                +#         # m = 2:  8 bytes => 11 characters.
                +#         Base64.urlsafe_encode64('12345678', padding: false) # => "MDEyMzQ1Njc"
                +#
                +# **Padding in Decode Methods**
                +#
                +# All of the Base64 decode methods support (but do not require) padding.
                +#
                +# Method Base64.decode64 does not check the size of the padding:
                +#
                +#     Base64.decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +#
                +# Method Base64.strict_decode64 strictly enforces padding size:
                +#
                +#     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +#     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +#     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +#
                +# Method Base64.urlsafe_decode64 allows padding in `str`, which if present, must
                +# be correct: see [Padding](Base64.html#module-Base64-label-Padding), above:
                +#
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +#
                +# ## Newlines
                +#
                +# An encoded string returned by Base64.encode64 or Base64.urlsafe_encode64 has
                +# an embedded newline character after each 60-character sequence, and, if
                +# non-empty, at the end:
                +#
                +#     # No newline if empty.
                +#     encoded = Base64.encode64("\x00" *  0)
                +#     encoded.index("\n") # => nil
                +#
                +#     # Newline at end of short output.
                +#     encoded = Base64.encode64("\x00" *  1)
                +#     encoded.size        # => 4
                +#     encoded.index("\n") # => 4
                +#
                +#     # Newline at end of longer output.
                +#     encoded = Base64.encode64("\x00" * 45)
                +#     encoded.size        # => 60
                +#     encoded.index("\n") # => 60
                +#
                +#     # Newlines embedded and at end of still longer output.
                +#     encoded = Base64.encode64("\x00" * 46)
                +#     encoded.size                          # => 65
                +#     encoded.rindex("\n")                  # => 65
                +#     encoded.split("\n").map {|s| s.size } # => [60, 4]
                +#
                +# The string to be encoded may itself contain newlines, which are encoded as
                +# Base64:
                +#
                +#       #   Base64.encode64("\n\n\n") # => "CgoK\n"
                +#     s = "This is line 1\nThis is line 2\n"
                +#     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +#
                +module Base64
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #     Base64.decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` are ignored; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.decode64("\x00\n-_") # => ""
                +  #
                +  # Padding in `str` (even if incorrect) is ignored:
                +  #
                +  #     Base64.decode64("MDEyMzQ1Njc")   # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +  #
                +  def self?.decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string ends with a newline character, and if sufficiently long
                +  # will have one or more embedded newline characters; see
                +  # [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #     Base64.encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq\nKg==\n"
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.encode64("\n\n\n") # => "CgoK\n"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #
                +  def self?.encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #     Base64.strict_decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` not allowed; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.strict_decode64("\n") # Raises ArgumentError
                +  #     Base64.strict_decode64('-')  # Raises ArgumentError
                +  #     Base64.strict_decode64('_')  # Raises ArgumentError
                +  #
                +  # Padding in `str`, if present, must be correct:
                +  #
                +  #     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +  #     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +  #
                +  def self?.strict_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.strict_encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.strict_encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg=="
                +  #     Base64.strict_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.strict_encode64("\n\n\n") # => "CgoK"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.strict_encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #
                +  def self?.strict_encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_decode64(str)
                +  # -->
                +  # Returns the decoding of an RFC-4648-compliant Base64-encoded string `str`:
                +  #
                +  # `str` may not contain non-Base64 characters; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_decode64('+')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64('/')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64("\n") # Raises ArgumentError.
                +  #
                +  # Padding in `str`, if present, must be correct: see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +  #
                +  def self?.urlsafe_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_encode64(bin, padding: true)
                +  # -->
                +  # Returns the RFC-4648-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 4648, the returned string will not contain the URL-unsafe characters
                +  # `+` or `/`, but instead may contain the URL-safe characters `-` and `_`; see
                +  # [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_encode64("\xFB\xEF\xBE") # => "----"
                +  #     Base64.urlsafe_encode64("\xFF\xFF\xFF") # => "____"
                +  #
                +  # By default, the returned string may have padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #
                +  # Optionally, you can suppress padding:
                +  #
                +  #     Base64.urlsafe_encode64('*', padding: false) # => "Kg"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #     Base64.urlsafe_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  def self?.urlsafe_encode64: (String bin, ?padding: boolish) -> String
                +end
      * Changed:
        lib/base64.rb
                --- /tmp/d20260408-320-fnp517/base64-0.2.0/lib/base64.rb	2026-04-08 03:05:58.762564558 +0000
                +++ /tmp/d20260408-320-fnp517/base64-0.3.0/lib/base64.rb	2026-04-08 03:05:58.763564553 +0000
                @@ -5 +5 @@
                -# - Encoding a binary string (containing non-ASCII characters)
                +# - \Encoding a binary string (containing non-ASCII characters)
                @@ -30 +30 @@
                -# == Encoding Character Sets
                +# == \Encoding Character Sets
                @@ -143 +143 @@
                -# \Method Base64.urlsafe_decode64 allows padding in +str+,
                +# \Method Base64.urlsafe_decode64 allows padding in the encoded string,
                @@ -186 +186 @@
                -  VERSION = "0.2.0"
                +  VERSION = "0.3.0"
                @@ -190 +190,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -222,0 +226,3 @@
                +  # :call-seq:
                +  #   Base64.decode(encoded_string) -> decoded_string
                +  #
                @@ -224 +230 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -229 +235 @@
                -  # Non-\Base64 characters in +str+ are ignored;
                +  # Non-\Base64 characters in +encoded_string+ are ignored;
                @@ -235 +241 @@
                -  # Padding in +str+ (even if incorrect) is ignored:
                +  # Padding in +encoded_string+ (even if incorrect) is ignored:
                @@ -245 +251,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.strict_encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -276,0 +286,3 @@
                +  # :call-seq:
                +  #   Base64.strict_decode64(encoded_string) -> decoded_string
                +  #
                @@ -278 +290 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -283 +295 @@
                -  # Non-\Base64 characters in +str+ not allowed;
                +  # Non-\Base64 characters in +encoded_string+ are not allowed;
                @@ -291 +303 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:
                @@ -301 +313,4 @@
                -  # Returns the RFC-4648-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.urlsafe_encode64(string) -> encoded_string
                +  #
                +  # Returns the RFC-4648-compliant \Base64-encoding of +string+.
                @@ -335 +350,4 @@
                -  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +str+:
                +  # :call-seq:
                +  #   Base64.urlsafe_decode64(encoded_string) -> decoded_string
                +  #
                +  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +encoded_string+:
                @@ -337 +355 @@
                -  # +str+ may not contain non-Base64 characters;
                +  # +encoded_string+ may not contain non-Base64 characters;
                @@ -344 +362 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
        BSDL
                --- /tmp/20260408-329-har2jo	2026-04-08 03:06:04.522681463 +0000
                +++ /tmp/d20260408-329-2csva0/base64-0.3.0/BSDL	2026-04-08 03:06:04.520681446 +0000
                @@ -0,0 +1,22 @@
                +Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
                +
                +Redistribution and use in source and binary forms, with or without
                +modification, are permitted provided that the following conditions
                +are met:
                +1. Redistributions of source code must retain the above copyright
                +   notice, this list of conditions and the following disclaimer.
                +2. Redistributions in binary form must reproduce the above copyright
                +   notice, this list of conditions and the following disclaimer in the
                +   documentation and/or other materials provided with the distribution.
                +
                +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
                +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
                +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +SUCH DAMAGE.
        COPYING
                --- /tmp/20260408-329-b9ap8	2026-04-08 03:06:04.524681481 +0000
                +++ /tmp/d20260408-329-2csva0/base64-0.3.0/COPYING	2026-04-08 03:06:04.520681446 +0000
                @@ -0,0 +1,56 @@
                +Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
                +You can redistribute it and/or modify it under either the terms of the
                +2-clause BSDL (see the file BSDL), or the conditions below:
                +
                +1. You may make and give away verbatim copies of the source form of the
                +   software without restriction, provided that you duplicate all of the
                +   original copyright notices and associated disclaimers.
                +
                +2. You may modify your copy of the software in any way, provided that
                +   you do at least ONE of the following:
                +
                +   a. place your modifications in the Public Domain or otherwise
                +      make them Freely Available, such as by posting said
                +      modifications to Usenet or an equivalent medium, or by allowing
                +      the author to include your modifications in the software.
                +
                +   b. use the modified software only within your corporation or
                +      organization.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +3. You may distribute the software in object code or binary form,
                +   provided that you do at least ONE of the following:
                +
                +   a. distribute the binaries and library files of the software,
                +      together with instructions (in the manual page or equivalent)
                +      on where to get the original distribution.
                +
                +   b. accompany the distribution with the machine-readable source of
                +      the software.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +4. You may modify and include the part of the software into any other
                +   software (possibly commercial).  But some files in the distribution
                +   are not written by the author, so that they are not under these terms.
                +
                +   For the list of those files and their copying conditions, see the
                +   file LEGAL.
                +
                +5. The scripts and library files supplied as input to or produced as
                +   output from the software do not automatically fall under the
                +   copyright of the software, but belong to whomever generated them,
                +   and may be sold commercially, and may be aggregated with this
                +   software.
                +
                +6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
                +   IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
                +   WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
                +   PURPOSE.
        LEGAL
                --- /tmp/20260408-329-uflihw	2026-04-08 03:06:04.527681507 +0000
                +++ /tmp/d20260408-329-2csva0/base64-0.3.0/LEGAL	2026-04-08 03:06:04.520681446 +0000
                @@ -0,0 +1,60 @@
                +# -*- rdoc -*-
                +
                += LEGAL NOTICE INFORMATION
                +--------------------------
                +
                +All the files in this distribution are covered under either the Ruby's
                +license (see the file COPYING) or public-domain except some files
                +mentioned below.
                +
                +== MIT License
                +>>>
                +      Permission is hereby granted, free of charge, to any person obtaining
                +      a copy of this software and associated documentation files (the
                +      "Software"), to deal in the Software without restriction, including
                +      without limitation the rights to use, copy, modify, merge, publish,
                +      distribute, sublicense, and/or sell copies of the Software, and to
                +      permit persons to whom the Software is furnished to do so, subject to
                +      the following conditions:
                +
                +      The above copyright notice and this permission notice shall be
                +      included in all copies or substantial portions of the Software.
                +
                +      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
                +      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
                +      MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
                +      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
                +      LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
                +      OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
                +      WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
                +
                +== Old-style BSD license
                +>>>
                +      Redistribution and use in source and binary forms, with or without
                +      modification, are permitted provided that the following conditions
                +      are met:
                +      1. Redistributions of source code must retain the above copyright
                +         notice, this list of conditions and the following disclaimer.
                +      2. Redistributions in binary form must reproduce the above copyright
                +         notice, this list of conditions and the following disclaimer in the
                +         documentation and/or other materials provided with the distribution.
                +      3. Neither the name of the University nor the names of its contributors
                +         may be used to endorse or promote products derived from this software
                +         without specific prior written permission.
                +
                +      THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
                +      ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +      IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +      ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
                +      FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +      DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +      OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +      HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +      LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +      OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +      SUCH DAMAGE.
                +
                +      IMPORTANT NOTE::
                +
                +      From ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change
                +      paragraph 3 above is now null and void.
        sig/base64.rbs
                --- /tmp/20260408-329-85rumk	2026-04-08 03:06:04.527681507 +0000
                +++ /tmp/d20260408-329-2csva0/base64-0.3.0/sig/base64.rbs	2026-04-08 03:06:04.521681455 +0000
                @@ -0,0 +1,355 @@
                +# <!-- rdoc-file=lib/base64.rb -->
                +# Module Base64 provides methods for:
                +#
                +# *   Encoding a binary string (containing non-ASCII characters) as a string of
                +#     printable ASCII characters.
                +# *   Decoding such an encoded string.
                +#
                +# Base64 is commonly used in contexts where binary data is not allowed or
                +# supported:
                +#
                +# *   Images in HTML or CSS files, or in URLs.
                +# *   Email attachments.
                +#
                +# A Base64-encoded string is about one-third larger that its source. See the
                +# [Wikipedia article](https://en.wikipedia.org/wiki/Base64) for more
                +# information.
                +#
                +# This module provides three pairs of encode/decode methods. Your choices among
                +# these methods should depend on:
                +#
                +# *   Which character set is to be used for encoding and decoding.
                +# *   Whether "padding" is to be used.
                +# *   Whether encoded strings are to contain newlines.
                +#
                +# Note: Examples on this page assume that the including program has executed:
                +#
                +#     require 'base64'
                +#
                +# ## Encoding Character Sets
                +#
                +# A Base64-encoded string consists only of characters from a 64-character set:
                +#
                +# *   `('A'..'Z')`.
                +# *   `('a'..'z')`.
                +# *   `('0'..'9')`.
                +# *   `=`, the 'padding' character.
                +# *   Either:
                +#     *   `%w[+ /]`:
                +#         [RFC-2045-compliant](https://datatracker.ietf.org/doc/html/rfc2045);
                +#         *not* safe for URLs.
                +#     *   `%w[- _]`:
                +#         [RFC-4648-compliant](https://datatracker.ietf.org/doc/html/rfc4648);
                +#         safe for URLs.
                +#
                +# If you are working with Base64-encoded strings that will come from or be put
                +# into URLs, you should choose this encoder-decoder pair of RFC-4648-compliant
                +# methods:
                +#
                +# *   Base64.urlsafe_encode64 and Base64.urlsafe_decode64.
                +#
                +# Otherwise, you may choose any of the pairs in this module, including the pair
                +# above, or the RFC-2045-compliant pairs:
                +#
                +# *   Base64.encode64 and Base64.decode64.
                +# *   Base64.strict_encode64 and Base64.strict_decode64.
                +#
                +# ## Padding
                +#
                +# Base64-encoding changes a triplet of input bytes into a quartet of output
                +# characters.
                +#
                +# **Padding in Encode Methods**
                +#
                +# Padding -- extending an encoded string with zero, one, or two trailing `=`
                +# characters -- is performed by methods Base64.encode64, Base64.strict_encode64,
                +# and, by default, Base64.urlsafe_encode64:
                +#
                +#     Base64.encode64('s')                         # => "cw==\n"
                +#     Base64.strict_encode64('s')                  # => "cw=="
                +#     Base64.urlsafe_encode64('s')                 # => "cw=="
                +#     Base64.urlsafe_encode64('s', padding: false) # => "cw"
                +#
                +# When padding is performed, the encoded string is always of length *4n*, where
                +# `n` is a non-negative integer:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.strict_encode64('123')      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.strict_encode64('123456')   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate padded output characters of length
                +#     *4(n+1)*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 8 characters.
                +#         Base64.strict_encode64('1234')     # => "MDEyMw=="
                +#         # n = 2:  7 bytes => 12 characters.
                +#         Base64.strict_encode64('1234567')  # => "MDEyMzQ1Ng=="
                +#
                +# *   Input bytes of length *3n+2* generate padded output characters of length
                +#     *4(n+1)*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 8 characters.
                +#         Base64.strict_encode64('12345')    # => "MDEyMzQ="
                +#         # n = 2:  8 bytes => 12 characters.
                +#         Base64.strict_encode64('12345678') # => "MDEyMzQ1Njc="
                +#
                +# When padding is suppressed, for a positive integer *n*:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.urlsafe_encode64('123', padding: false)      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.urlsafe_encode64('123456', padding: false)   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate unpadded output characters of length
                +#     *4n+2*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 6 characters.
                +#         Base64.urlsafe_encode64('1234', padding: false)     # => "MDEyMw"
                +#         # n = 2:  7 bytes => 10 characters.
                +#         Base64.urlsafe_encode64('1234567', padding: false)  # => "MDEyMzQ1Ng"
                +#
                +# *   Input bytes of length *3n+2* generate unpadded output characters of length
                +#     *4n+3*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 7 characters.
                +#         Base64.urlsafe_encode64('12345', padding: false)    # => "MDEyMzQ"
                +#         # m = 2:  8 bytes => 11 characters.
                +#         Base64.urlsafe_encode64('12345678', padding: false) # => "MDEyMzQ1Njc"
                +#
                +# **Padding in Decode Methods**
                +#
                +# All of the Base64 decode methods support (but do not require) padding.
                +#
                +# Method Base64.decode64 does not check the size of the padding:
                +#
                +#     Base64.decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +#
                +# Method Base64.strict_decode64 strictly enforces padding size:
                +#
                +#     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +#     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +#     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +#
                +# Method Base64.urlsafe_decode64 allows padding in `str`, which if present, must
                +# be correct: see [Padding](Base64.html#module-Base64-label-Padding), above:
                +#
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +#
                +# ## Newlines
                +#
                +# An encoded string returned by Base64.encode64 or Base64.urlsafe_encode64 has
                +# an embedded newline character after each 60-character sequence, and, if
                +# non-empty, at the end:
                +#
                +#     # No newline if empty.
                +#     encoded = Base64.encode64("\x00" *  0)
                +#     encoded.index("\n") # => nil
                +#
                +#     # Newline at end of short output.
                +#     encoded = Base64.encode64("\x00" *  1)
                +#     encoded.size        # => 4
                +#     encoded.index("\n") # => 4
                +#
                +#     # Newline at end of longer output.
                +#     encoded = Base64.encode64("\x00" * 45)
                +#     encoded.size        # => 60
                +#     encoded.index("\n") # => 60
                +#
                +#     # Newlines embedded and at end of still longer output.
                +#     encoded = Base64.encode64("\x00" * 46)
                +#     encoded.size                          # => 65
                +#     encoded.rindex("\n")                  # => 65
                +#     encoded.split("\n").map {|s| s.size } # => [60, 4]
                +#
                +# The string to be encoded may itself contain newlines, which are encoded as
                +# Base64:
                +#
                +#       #   Base64.encode64("\n\n\n") # => "CgoK\n"
                +#     s = "This is line 1\nThis is line 2\n"
                +#     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +#
                +module Base64
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #     Base64.decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` are ignored; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.decode64("\x00\n-_") # => ""
                +  #
                +  # Padding in `str` (even if incorrect) is ignored:
                +  #
                +  #     Base64.decode64("MDEyMzQ1Njc")   # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +  #
                +  def self?.decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string ends with a newline character, and if sufficiently long
                +  # will have one or more embedded newline characters; see
                +  # [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #     Base64.encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq\nKg==\n"
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.encode64("\n\n\n") # => "CgoK\n"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #
                +  def self?.encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #     Base64.strict_decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` not allowed; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.strict_decode64("\n") # Raises ArgumentError
                +  #     Base64.strict_decode64('-')  # Raises ArgumentError
                +  #     Base64.strict_decode64('_')  # Raises ArgumentError
                +  #
                +  # Padding in `str`, if present, must be correct:
                +  #
                +  #     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +  #     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +  #
                +  def self?.strict_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.strict_encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.strict_encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg=="
                +  #     Base64.strict_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.strict_encode64("\n\n\n") # => "CgoK"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.strict_encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #
                +  def self?.strict_encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_decode64(str)
                +  # -->
                +  # Returns the decoding of an RFC-4648-compliant Base64-encoded string `str`:
                +  #
                +  # `str` may not contain non-Base64 characters; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_decode64('+')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64('/')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64("\n") # Raises ArgumentError.
                +  #
                +  # Padding in `str`, if present, must be correct: see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +  #
                +  def self?.urlsafe_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_encode64(bin, padding: true)
                +  # -->
                +  # Returns the RFC-4648-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 4648, the returned string will not contain the URL-unsafe characters
                +  # `+` or `/`, but instead may contain the URL-safe characters `-` and `_`; see
                +  # [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_encode64("\xFB\xEF\xBE") # => "----"
                +  #     Base64.urlsafe_encode64("\xFF\xFF\xFF") # => "____"
                +  #
                +  # By default, the returned string may have padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #
                +  # Optionally, you can suppress padding:
                +  #
                +  #     Base64.urlsafe_encode64('*', padding: false) # => "Kg"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #     Base64.urlsafe_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  def self?.urlsafe_encode64: (String bin, ?padding: boolish) -> String
                +end
      * Changed:
        lib/base64.rb
                --- /tmp/d20260408-329-2csva0/base64-0.2.0/lib/base64.rb	2026-04-08 03:06:04.520681446 +0000
                +++ /tmp/d20260408-329-2csva0/base64-0.3.0/lib/base64.rb	2026-04-08 03:06:04.521681455 +0000
                @@ -5 +5 @@
                -# - Encoding a binary string (containing non-ASCII characters)
                +# - \Encoding a binary string (containing non-ASCII characters)
                @@ -30 +30 @@
                -# == Encoding Character Sets
                +# == \Encoding Character Sets
                @@ -143 +143 @@
                -# \Method Base64.urlsafe_decode64 allows padding in +str+,
                +# \Method Base64.urlsafe_decode64 allows padding in the encoded string,
                @@ -186 +186 @@
                -  VERSION = "0.2.0"
                +  VERSION = "0.3.0"
                @@ -190 +190,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -222,0 +226,3 @@
                +  # :call-seq:
                +  #   Base64.decode(encoded_string) -> decoded_string
                +  #
                @@ -224 +230 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -229 +235 @@
                -  # Non-\Base64 characters in +str+ are ignored;
                +  # Non-\Base64 characters in +encoded_string+ are ignored;
                @@ -235 +241 @@
                -  # Padding in +str+ (even if incorrect) is ignored:
                +  # Padding in +encoded_string+ (even if incorrect) is ignored:
                @@ -245 +251,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.strict_encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -276,0 +286,3 @@
                +  # :call-seq:
                +  #   Base64.strict_decode64(encoded_string) -> decoded_string
                +  #
                @@ -278 +290 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -283 +295 @@
                -  # Non-\Base64 characters in +str+ not allowed;
                +  # Non-\Base64 characters in +encoded_string+ are not allowed;
                @@ -291 +303 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:
                @@ -301 +313,4 @@
                -  # Returns the RFC-4648-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.urlsafe_encode64(string) -> encoded_string
                +  #
                +  # Returns the RFC-4648-compliant \Base64-encoding of +string+.
                @@ -335 +350,4 @@
                -  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +str+:
                +  # :call-seq:
                +  #   Base64.urlsafe_decode64(encoded_string) -> decoded_string
                +  #
                +  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +encoded_string+:
                @@ -337 +355 @@
                -  # +str+ may not contain non-Base64 characters;
                +  # +encoded_string+ may not contain non-Base64 characters;
                @@ -344 +362 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT rubygems_version:
    3.2.3: 3.6.9
    3.2.6: 4.0.6
  DIFFERENT version:
    3.2.3: 3.2.3
    3.2.6: 3.2.6
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0
            lib/rack/directory.rb +6/-3
            lib/rack/files.rb +1/-1
            lib/rack/mock_response.rb +11/-2
            lib/rack/multipart/parser.rb +44/-3
            lib/rack/request.rb +2/-2
            lib/rack/sendfile.rb +2/-2
            lib/rack/static.rb +7/-3
            lib/rack/utils.rb +107/-15
            lib/rack/version.rb +1/-1
  DIFFERENT extra_rdoc_files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT date:
    0.2.0: 2023-11-07 00:00:00 UTC
    0.3.0: 1980-01-02 00:00:00 UTC
  DIFFERENT metadata:
    0.2.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64"}
    0.3.0: {"homepage_uri" => "https://github.com/ruby/base64", "source_code_uri" => "https://github.com/ruby/base64", "changelog_uri" => "https://github.com/ruby/base64/releases"}
  DIFFERENT rubygems_version:
    0.2.0: 3.5.0.dev
    0.3.0: 3.6.7
  DIFFERENT version:
    0.2.0: 0.2.0
    0.3.0: 0.3.0
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
            BSDL +22/-0
            COPYING +56/-0
            LEGAL +60/-0
            sig/base64.rbs +355/-0
      * Changed:
            lib/base64.rb +34/-16

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT rubygems_version:
    3.2.3: 3.6.9
    3.2.6: 4.0.6
  DIFFERENT version:
    3.2.3: 3.2.3
    3.2.6: 3.2.6
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0
            lib/rack/directory.rb +6/-3
            lib/rack/files.rb +1/-1
            lib/rack/mock_response.rb +11/-2
            lib/rack/multipart/parser.rb +44/-3
            lib/rack/request.rb +2/-2
            lib/rack/sendfile.rb +2/-2
            lib/rack/static.rb +7/-3
            lib/rack/utils.rb +107/-15
            lib/rack/version.rb +1/-1
  DIFFERENT extra_rdoc_files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0

1 similar comment
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT rubygems_version:
    3.2.3: 3.6.9
    3.2.6: 4.0.6
  DIFFERENT version:
    3.2.3: 3.2.3
    3.2.6: 3.2.6
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0
            lib/rack/directory.rb +6/-3
            lib/rack/files.rb +1/-1
            lib/rack/mock_response.rb +11/-2
            lib/rack/multipart/parser.rb +44/-3
            lib/rack/request.rb +2/-2
            lib/rack/sendfile.rb +2/-2
            lib/rack/static.rb +7/-3
            lib/rack/utils.rb +107/-15
            lib/rack/version.rb +1/-1
  DIFFERENT extra_rdoc_files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
        CHANGELOG.md
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/CHANGELOG.md	2026-04-08 03:06:24.115418029 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/CHANGELOG.md	2026-04-08 03:06:24.125417974 +0000
                @@ -4,0 +5,35 @@
                +## [3.2.6] - 2026-04-01
                +
                +### Security
                +
                +- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
                +- [CVE-2026-34230](https://github.com/advisories/GHSA-v569-hp3g-36wr) Avoid O(n^2) algorithm in `Rack::Utils.select_best_encoding` which could lead to denial of service.
                +- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
                +- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
                +- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
                +- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
                +- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
                +- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
                +- [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) `Rack::Sendfile` header-based `X-Accel-Mapping` regex injection enables unauthorized `X-Accel-Redirect`.
                +- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
                +- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
                +- [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) Quadratic-time multipart header parsing allows denial of service via escape-heavy quoted parameters.
                +- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
                +
                +## [3.2.5] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +### Fixed
                +
                +- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
                +
                +## [3.2.4] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -76,0 +112,13 @@
                +## [3.1.20] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [3.1.19] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -459,0 +508,13 @@
                +
                +## [2.2.22] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [2.2.21] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
        lib/rack/directory.rb
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/lib/rack/directory.rb	2026-04-08 03:06:24.119418007 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/lib/rack/directory.rb	2026-04-08 03:06:24.127417963 +0000
                @@ -20 +20 @@
                -    DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                +    DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                @@ -54 +54 @@
                -        show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
                +        show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
                @@ -84,0 +85 @@
                +      @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
                @@ -121 +122,3 @@
                -      return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
                +
                +      expanded_path = ::File.expand_path(::File.join(@root, path_info))
                +      return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
        lib/rack/files.rb
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/lib/rack/files.rb	2026-04-08 03:06:24.119418007 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/lib/rack/files.rb	2026-04-08 03:06:24.128417957 +0000
                @@ -197 +197 @@
                -          CONTENT_LENGTH => body.size.to_s,
                +          CONTENT_LENGTH => body.bytesize.to_s,
        lib/rack/mock_response.rb
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/lib/rack/mock_response.rb	2026-04-08 03:06:24.121417996 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/lib/rack/mock_response.rb	2026-04-08 03:06:24.129417952 +0000
                @@ -2,0 +3 @@
                +require 'stringio'
                @@ -85,2 +86,10 @@
                -      @body.each do |chunk|
                -        buffer << chunk
                +      begin
                +        if @body.respond_to?(:each)
                +          @body.each do |chunk|
                +            buffer << chunk
                +          end
                +        else
                +          @body.call(StringIO.new(buffer))
                +        end
                +      ensure
                +        @body.close if @body.respond_to?(:close)
        lib/rack/multipart/parser.rb
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/lib/rack/multipart/parser.rb	2026-04-08 03:06:24.121417996 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/lib/rack/multipart/parser.rb	2026-04-08 03:06:24.129417952 +0000
                @@ -36 +36 @@
                -    MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
                +    MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
                @@ -82,0 +83,7 @@
                +      bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
                +      PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
                +      private_constant :PARSER_BYTESIZE_LIMIT
                +
                +      CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
                +      private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +
                @@ -119 +126,9 @@
                -        data[1]
                +
                +        unless data[1].empty?
                +          raise Error, "whitespace between boundary parameter name and equal sign"
                +        end
                +        if data.post_match.match?(/boundary\s*=/i)
                +          raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
                +        end
                +
                +        data[2]
                @@ -127,0 +143,4 @@
                +        if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
                +          raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +        end
                +
                @@ -243,0 +263,2 @@
                +        @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
                +        @content_disposition_quoted_escapes = 0
                @@ -254,0 +276 @@
                +        @total_bytes_read &&= nil if io.is_a?(BoundedIO)
                @@ -292,0 +315,6 @@
                +        if @total_bytes_read
                +          @total_bytes_read += content.bytesize
                +          if @total_bytes_read > PARSER_BYTESIZE_LIMIT
                +            raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +          end
                +        end
                @@ -341,0 +370,3 @@
                +      OBS_UNFOLD = /\r\n([ \t])/
                +      private_constant :OBS_UNFOLD
                +
                @@ -345,0 +377,2 @@
                +          content_type.gsub!(OBS_UNFOLD, '\1') if content_type
                +
                @@ -348,0 +382,3 @@
                +            # Implement OBS unfolding (RFC 5322 Section 2.2.3)
                +            disposition.gsub!(OBS_UNFOLD, '\1')
                +
                @@ -385,0 +422,5 @@
                +                  @content_disposition_quoted_escapes += 1
                +                  if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +                    raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
                +                  end
                +
                @@ -454 +495 @@
                -          raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
                +          raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
        lib/rack/request.rb
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/lib/rack/request.rb	2026-04-08 03:06:24.122417990 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/lib/rack/request.rb	2026-04-08 03:06:24.130417946 +0000
                @@ -726,2 +726,2 @@
                -          # Match any other printable string (except square brackets) as a hostname
                -          (?<address>[[[:graph:]&&[^\[\]]]]*?)
                +          # Match characters allowed by RFC 3986 Section 3.2.2
                +          (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
        lib/rack/sendfile.rb
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/lib/rack/sendfile.rb	2026-04-08 03:06:24.122417990 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/lib/rack/sendfile.rb	2026-04-08 03:06:24.131417940 +0000
                @@ -54 +54 @@
                -  # that it maps to. The middleware performs a simple substitution on the
                +  # that it maps to. The middleware performs a case-insensitive substitution on the
                @@ -189 +189 @@
                -          new_path = path.sub(/\A#{internal}/i, external)
                +          new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
        lib/rack/static.rb
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/lib/rack/static.rb	2026-04-08 03:06:24.123417985 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/lib/rack/static.rb	2026-04-08 03:06:24.131417940 +0000
                @@ -95,0 +96,3 @@
                +      if @urls.kind_of?(Array)
                +        @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
                +      end
                @@ -118 +121 @@
                -      @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
                +      @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
                @@ -167,0 +171,2 @@
                +      path = ::Rack::Utils.unescape_path(path)
                +
                @@ -175 +179,0 @@
                -          path = ::Rack::Utils.unescape(path)
                @@ -178 +182 @@
                -          /\.(#{rule.join('|')})\z/.match?(path)
                +          /\.#{Regexp.union(rule)}\z/.match?(path)
        lib/rack/utils.rb
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/lib/rack/utils.rb	2026-04-08 03:06:24.124417979 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/lib/rack/utils.rb	2026-04-08 03:06:24.131417940 +0000
                @@ -149,3 +149,2 @@
                -    def forwarded_values(forwarded_header)
                -      return nil unless forwarded_header
                -      forwarded_header = forwarded_header.to_s.gsub("\n", ";")
                +    ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
                +    private_constant :ALLOWED_FORWARED_PARAMS
                @@ -153,5 +152,59 @@
                -      forwarded_header.split(';').each_with_object({}) do |field, values|
                -        field.split(',').each do |pair|
                -          pair = pair.split('=').map(&:strip).join('=')
                -          return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
                -          (values[$1.downcase.to_sym] ||= []) << $2
                +    def forwarded_values(forwarded_header)
                +      return unless forwarded_header
                +      header = forwarded_header.to_s.tr("\n", ";")
                +      header.sub!(/\A[\s;,]+/, '')
                +      num_params = num_escapes = 0
                +      max_params = max_escapes = 1024
                +      params = {}
                +
                +      # Parse parameter list
                +      while i = header.index('=')
                +        # Only parse up to max parameters, to avoid potential denial of service
                +        num_params += 1
                +        return if num_params > max_params
                +
                +        # Found end of parameter name, ensure forward progress in loop
                +        param = header.slice!(0, i+1)
                +
                +        # Remove ending equals and preceding whitespace from parameter name
                +        param.chomp!('=')
                +        param.strip!
                +        param.downcase!
                +        return unless param = ALLOWED_FORWARED_PARAMS[param]
                +
                +        if header[0] == '"'
                +          # Parameter value is quoted, parse it, handling backslash escapes
                +          header.slice!(0, 1)
                +          value = String.new
                +
                +          while i = header.index(/(["\\])/)
                +            c = $1
                +
                +            # Append all content until ending quote or escape
                +            value << header.slice!(0, i)
                +
                +            # Remove either backslash or ending quote,
                +            # ensures forward progress in loop
                +            header.slice!(0, 1)
                +
                +            # stop parsing parameter value if found ending quote
                +            break if c == '"'
                +
                +            # Only allow up to max escapes, to avoid potential denial of service
                +            num_escapes += 1
                +            return if num_escapes > max_escapes
                +            escaped_char = header.slice!(0, 1)
                +            value << escaped_char
                +          end
                +        else
                +          if i = header.index(/[;,]/)
                +            # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
                +            value = header.slice!(0, i)
                +            value.sub!(/[\s;,]+\z/, '')
                +          else
                +            # If no ending semicolon, assume remainder of line is value and stop parsing
                +            header.strip!
                +            value = header
                +            header = ''
                +          end
                +          value.lstrip!
                @@ -158,0 +212,5 @@
                +
                +        (params[param] ||= []) << value
                +
                +        # skip trailing semicolons/commas/whitespace, to proceed to next parameter
                +        header.sub!(/\A[\s;,]+/, '') unless header.empty?
                @@ -159,0 +218,2 @@
                +
                +      params
                @@ -195,0 +256,18 @@
                +    # Given an array of available encoding strings, and an array of
                +    # acceptable encodings for a request, where each element of the
                +    # acceptable encodings array is an array where the first element
                +    # is an encoding name and the second element is the numeric
                +    # priority for the encoding, return the available encoding with
                +    # the highest priority.
                +    #
                +    # The accept_encoding argument is typically generated by calling
                +    # Request#accept_encoding.
                +    #
                +    # Example:
                +    #
                +    #   select_best_encoding(%w(compress gzip identity),
                +    #                        [["compress", 0.5], ["gzip", 1.0]])
                +    #   # => "gzip"
                +    #
                +    # To reduce denial of service potential, only the first 16
                +    # acceptable encodings are considered.
                @@ -198,0 +277,2 @@
                +      # Only process the first 16 encodings
                +      accept_encoding = accept_encoding[0...16]
                @@ -199,0 +280 @@
                +      wildcard_seen = false
                @@ -205,2 +286,5 @@
                -          (available_encodings - accept_encoding.map(&:first)).each do |m2|
                -            expanded_accept_encoding << [m2, q, preference]
                +          unless wildcard_seen
                +            (available_encodings - accept_encoding.map(&:first)).each do |m2|
                +              expanded_accept_encoding << [m2, q, preference]
                +            end
                +            wildcard_seen = true
                @@ -214 +298,7 @@
                -        .sort_by { |_, q, p| [-q, p] }
                +        .sort do |(_, q1, p1), (_, q2, p2)|
                +          if r = (q1 <=> q2).nonzero?
                +            -r
                +          else
                +            (p1 <=> p2).nonzero? || 0
                +          end
                +        end
                @@ -402,2 +492,2 @@
                -    def byte_ranges(env, size)
                -      get_byte_ranges env['HTTP_RANGE'], size
                +    def byte_ranges(env, size, max_ranges: 100)
                +      get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
                @@ -406 +496 @@
                -    def get_byte_ranges(http_range, size)
                +    def get_byte_ranges(http_range, size, max_ranges: 100)
                @@ -410,0 +501,2 @@
                +      byte_range = $1
                +      return nil if byte_range.count(',') >= max_ranges
                @@ -412 +504 @@
                -      $1.split(/,[ \t]*/).each do |range_spec|
                +      byte_range.split(/,[ \t]*/).each do |range_spec|
        lib/rack/version.rb
                --- /tmp/d20260408-625-4jkd49/rack-3.2.3/lib/rack/version.rb	2026-04-08 03:06:24.124417979 +0000
                +++ /tmp/d20260408-625-4jkd49/rack-3.2.6/lib/rack/version.rb	2026-04-08 03:06:24.132417935 +0000
                @@ -9 +9 @@
                -  VERSION = "3.2.3"
                +  VERSION = "3.2.6"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT rubygems_version:
    3.2.3: 3.6.9
    3.2.6: 4.0.6
  DIFFERENT version:
    3.2.3: 3.2.3
    3.2.6: 3.2.6
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0
            lib/rack/directory.rb +6/-3
            lib/rack/files.rb +1/-1
            lib/rack/mock_response.rb +11/-2
            lib/rack/multipart/parser.rb +44/-3
            lib/rack/request.rb +2/-2
            lib/rack/sendfile.rb +2/-2
            lib/rack/static.rb +7/-3
            lib/rack/utils.rb +107/-15
            lib/rack/version.rb +1/-1
  DIFFERENT extra_rdoc_files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
        CHANGELOG.md
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/CHANGELOG.md	2026-04-08 03:06:35.730024090 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/CHANGELOG.md	2026-04-08 03:06:35.738024230 +0000
                @@ -4,0 +5,35 @@
                +## [3.2.6] - 2026-04-01
                +
                +### Security
                +
                +- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
                +- [CVE-2026-34230](https://github.com/advisories/GHSA-v569-hp3g-36wr) Avoid O(n^2) algorithm in `Rack::Utils.select_best_encoding` which could lead to denial of service.
                +- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
                +- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
                +- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
                +- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
                +- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
                +- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
                +- [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) `Rack::Sendfile` header-based `X-Accel-Mapping` regex injection enables unauthorized `X-Accel-Redirect`.
                +- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
                +- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
                +- [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) Quadratic-time multipart header parsing allows denial of service via escape-heavy quoted parameters.
                +- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
                +
                +## [3.2.5] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +### Fixed
                +
                +- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
                +
                +## [3.2.4] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -76,0 +112,13 @@
                +## [3.1.20] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [3.1.19] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -459,0 +508,13 @@
                +
                +## [2.2.22] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [2.2.21] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
        lib/rack/directory.rb
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/lib/rack/directory.rb	2026-04-08 03:06:35.733024142 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/lib/rack/directory.rb	2026-04-08 03:06:35.743024317 +0000
                @@ -20 +20 @@
                -    DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                +    DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                @@ -54 +54 @@
                -        show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
                +        show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
                @@ -84,0 +85 @@
                +      @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
                @@ -121 +122,3 @@
                -      return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
                +
                +      expanded_path = ::File.expand_path(::File.join(@root, path_info))
                +      return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
        lib/rack/files.rb
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/lib/rack/files.rb	2026-04-08 03:06:35.734024160 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/lib/rack/files.rb	2026-04-08 03:06:35.743024317 +0000
                @@ -197 +197 @@
                -          CONTENT_LENGTH => body.size.to_s,
                +          CONTENT_LENGTH => body.bytesize.to_s,
        lib/rack/mock_response.rb
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/lib/rack/mock_response.rb	2026-04-08 03:06:35.735024177 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/lib/rack/mock_response.rb	2026-04-08 03:06:35.744024334 +0000
                @@ -2,0 +3 @@
                +require 'stringio'
                @@ -85,2 +86,10 @@
                -      @body.each do |chunk|
                -        buffer << chunk
                +      begin
                +        if @body.respond_to?(:each)
                +          @body.each do |chunk|
                +            buffer << chunk
                +          end
                +        else
                +          @body.call(StringIO.new(buffer))
                +        end
                +      ensure
                +        @body.close if @body.respond_to?(:close)
        lib/rack/multipart/parser.rb
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/lib/rack/multipart/parser.rb	2026-04-08 03:06:35.735024177 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/lib/rack/multipart/parser.rb	2026-04-08 03:06:35.744024334 +0000
                @@ -36 +36 @@
                -    MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
                +    MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
                @@ -82,0 +83,7 @@
                +      bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
                +      PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
                +      private_constant :PARSER_BYTESIZE_LIMIT
                +
                +      CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
                +      private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +
                @@ -119 +126,9 @@
                -        data[1]
                +
                +        unless data[1].empty?
                +          raise Error, "whitespace between boundary parameter name and equal sign"
                +        end
                +        if data.post_match.match?(/boundary\s*=/i)
                +          raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
                +        end
                +
                +        data[2]
                @@ -127,0 +143,4 @@
                +        if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
                +          raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +        end
                +
                @@ -243,0 +263,2 @@
                +        @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
                +        @content_disposition_quoted_escapes = 0
                @@ -254,0 +276 @@
                +        @total_bytes_read &&= nil if io.is_a?(BoundedIO)
                @@ -292,0 +315,6 @@
                +        if @total_bytes_read
                +          @total_bytes_read += content.bytesize
                +          if @total_bytes_read > PARSER_BYTESIZE_LIMIT
                +            raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +          end
                +        end
                @@ -341,0 +370,3 @@
                +      OBS_UNFOLD = /\r\n([ \t])/
                +      private_constant :OBS_UNFOLD
                +
                @@ -345,0 +377,2 @@
                +          content_type.gsub!(OBS_UNFOLD, '\1') if content_type
                +
                @@ -348,0 +382,3 @@
                +            # Implement OBS unfolding (RFC 5322 Section 2.2.3)
                +            disposition.gsub!(OBS_UNFOLD, '\1')
                +
                @@ -385,0 +422,5 @@
                +                  @content_disposition_quoted_escapes += 1
                +                  if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +                    raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
                +                  end
                +
                @@ -454 +495 @@
                -          raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
                +          raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
        lib/rack/request.rb
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/lib/rack/request.rb	2026-04-08 03:06:35.736024195 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/lib/rack/request.rb	2026-04-08 03:06:35.745024352 +0000
                @@ -726,2 +726,2 @@
                -          # Match any other printable string (except square brackets) as a hostname
                -          (?<address>[[[:graph:]&&[^\[\]]]]*?)
                +          # Match characters allowed by RFC 3986 Section 3.2.2
                +          (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
        lib/rack/sendfile.rb
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/lib/rack/sendfile.rb	2026-04-08 03:06:35.737024212 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/lib/rack/sendfile.rb	2026-04-08 03:06:35.746024369 +0000
                @@ -54 +54 @@
                -  # that it maps to. The middleware performs a simple substitution on the
                +  # that it maps to. The middleware performs a case-insensitive substitution on the
                @@ -189 +189 @@
                -          new_path = path.sub(/\A#{internal}/i, external)
                +          new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
        lib/rack/static.rb
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/lib/rack/static.rb	2026-04-08 03:06:35.737024212 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/lib/rack/static.rb	2026-04-08 03:06:35.747024387 +0000
                @@ -95,0 +96,3 @@
                +      if @urls.kind_of?(Array)
                +        @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
                +      end
                @@ -118 +121 @@
                -      @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
                +      @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
                @@ -167,0 +171,2 @@
                +      path = ::Rack::Utils.unescape_path(path)
                +
                @@ -175 +179,0 @@
                -          path = ::Rack::Utils.unescape(path)
                @@ -178 +182 @@
                -          /\.(#{rule.join('|')})\z/.match?(path)
                +          /\.#{Regexp.union(rule)}\z/.match?(path)
        lib/rack/utils.rb
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/lib/rack/utils.rb	2026-04-08 03:06:35.738024230 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/lib/rack/utils.rb	2026-04-08 03:06:35.748024404 +0000
                @@ -149,3 +149,2 @@
                -    def forwarded_values(forwarded_header)
                -      return nil unless forwarded_header
                -      forwarded_header = forwarded_header.to_s.gsub("\n", ";")
                +    ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
                +    private_constant :ALLOWED_FORWARED_PARAMS
                @@ -153,5 +152,59 @@
                -      forwarded_header.split(';').each_with_object({}) do |field, values|
                -        field.split(',').each do |pair|
                -          pair = pair.split('=').map(&:strip).join('=')
                -          return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
                -          (values[$1.downcase.to_sym] ||= []) << $2
                +    def forwarded_values(forwarded_header)
                +      return unless forwarded_header
                +      header = forwarded_header.to_s.tr("\n", ";")
                +      header.sub!(/\A[\s;,]+/, '')
                +      num_params = num_escapes = 0
                +      max_params = max_escapes = 1024
                +      params = {}
                +
                +      # Parse parameter list
                +      while i = header.index('=')
                +        # Only parse up to max parameters, to avoid potential denial of service
                +        num_params += 1
                +        return if num_params > max_params
                +
                +        # Found end of parameter name, ensure forward progress in loop
                +        param = header.slice!(0, i+1)
                +
                +        # Remove ending equals and preceding whitespace from parameter name
                +        param.chomp!('=')
                +        param.strip!
                +        param.downcase!
                +        return unless param = ALLOWED_FORWARED_PARAMS[param]
                +
                +        if header[0] == '"'
                +          # Parameter value is quoted, parse it, handling backslash escapes
                +          header.slice!(0, 1)
                +          value = String.new
                +
                +          while i = header.index(/(["\\])/)
                +            c = $1
                +
                +            # Append all content until ending quote or escape
                +            value << header.slice!(0, i)
                +
                +            # Remove either backslash or ending quote,
                +            # ensures forward progress in loop
                +            header.slice!(0, 1)
                +
                +            # stop parsing parameter value if found ending quote
                +            break if c == '"'
                +
                +            # Only allow up to max escapes, to avoid potential denial of service
                +            num_escapes += 1
                +            return if num_escapes > max_escapes
                +            escaped_char = header.slice!(0, 1)
                +            value << escaped_char
                +          end
                +        else
                +          if i = header.index(/[;,]/)
                +            # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
                +            value = header.slice!(0, i)
                +            value.sub!(/[\s;,]+\z/, '')
                +          else
                +            # If no ending semicolon, assume remainder of line is value and stop parsing
                +            header.strip!
                +            value = header
                +            header = ''
                +          end
                +          value.lstrip!
                @@ -158,0 +212,5 @@
                +
                +        (params[param] ||= []) << value
                +
                +        # skip trailing semicolons/commas/whitespace, to proceed to next parameter
                +        header.sub!(/\A[\s;,]+/, '') unless header.empty?
                @@ -159,0 +218,2 @@
                +
                +      params
                @@ -195,0 +256,18 @@
                +    # Given an array of available encoding strings, and an array of
                +    # acceptable encodings for a request, where each element of the
                +    # acceptable encodings array is an array where the first element
                +    # is an encoding name and the second element is the numeric
                +    # priority for the encoding, return the available encoding with
                +    # the highest priority.
                +    #
                +    # The accept_encoding argument is typically generated by calling
                +    # Request#accept_encoding.
                +    #
                +    # Example:
                +    #
                +    #   select_best_encoding(%w(compress gzip identity),
                +    #                        [["compress", 0.5], ["gzip", 1.0]])
                +    #   # => "gzip"
                +    #
                +    # To reduce denial of service potential, only the first 16
                +    # acceptable encodings are considered.
                @@ -198,0 +277,2 @@
                +      # Only process the first 16 encodings
                +      accept_encoding = accept_encoding[0...16]
                @@ -199,0 +280 @@
                +      wildcard_seen = false
                @@ -205,2 +286,5 @@
                -          (available_encodings - accept_encoding.map(&:first)).each do |m2|
                -            expanded_accept_encoding << [m2, q, preference]
                +          unless wildcard_seen
                +            (available_encodings - accept_encoding.map(&:first)).each do |m2|
                +              expanded_accept_encoding << [m2, q, preference]
                +            end
                +            wildcard_seen = true
                @@ -214 +298,7 @@
                -        .sort_by { |_, q, p| [-q, p] }
                +        .sort do |(_, q1, p1), (_, q2, p2)|
                +          if r = (q1 <=> q2).nonzero?
                +            -r
                +          else
                +            (p1 <=> p2).nonzero? || 0
                +          end
                +        end
                @@ -402,2 +492,2 @@
                -    def byte_ranges(env, size)
                -      get_byte_ranges env['HTTP_RANGE'], size
                +    def byte_ranges(env, size, max_ranges: 100)
                +      get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
                @@ -406 +496 @@
                -    def get_byte_ranges(http_range, size)
                +    def get_byte_ranges(http_range, size, max_ranges: 100)
                @@ -410,0 +501,2 @@
                +      byte_range = $1
                +      return nil if byte_range.count(',') >= max_ranges
                @@ -412 +504 @@
                -      $1.split(/,[ \t]*/).each do |range_spec|
                +      byte_range.split(/,[ \t]*/).each do |range_spec|
        lib/rack/version.rb
                --- /tmp/d20260408-629-fd040u/rack-3.2.3/lib/rack/version.rb	2026-04-08 03:06:35.738024230 +0000
                +++ /tmp/d20260408-629-fd040u/rack-3.2.6/lib/rack/version.rb	2026-04-08 03:06:35.748024404 +0000
                @@ -9 +9 @@
                -  VERSION = "3.2.3"
                +  VERSION = "3.2.6"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff base64 0.2.0 0.3.0

Compared versions: ["0.2.0", "0.3.0"]
  DIFFERENT files:
    0.2.0->0.3.0:
      * Deleted:
            LICENSE.txt
      * Added:
        BSDL
                --- /tmp/20260408-331-nu6qzb	2026-04-08 03:06:38.256430029 +0000
                +++ /tmp/d20260408-331-a583yb/base64-0.3.0/BSDL	2026-04-08 03:06:38.252430014 +0000
                @@ -0,0 +1,22 @@
                +Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
                +
                +Redistribution and use in source and binary forms, with or without
                +modification, are permitted provided that the following conditions
                +are met:
                +1. Redistributions of source code must retain the above copyright
                +   notice, this list of conditions and the following disclaimer.
                +2. Redistributions in binary form must reproduce the above copyright
                +   notice, this list of conditions and the following disclaimer in the
                +   documentation and/or other materials provided with the distribution.
                +
                +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
                +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
                +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +SUCH DAMAGE.
        COPYING
                --- /tmp/20260408-331-pwwwif	2026-04-08 03:06:38.259430040 +0000
                +++ /tmp/d20260408-331-a583yb/base64-0.3.0/COPYING	2026-04-08 03:06:38.254430021 +0000
                @@ -0,0 +1,56 @@
                +Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
                +You can redistribute it and/or modify it under either the terms of the
                +2-clause BSDL (see the file BSDL), or the conditions below:
                +
                +1. You may make and give away verbatim copies of the source form of the
                +   software without restriction, provided that you duplicate all of the
                +   original copyright notices and associated disclaimers.
                +
                +2. You may modify your copy of the software in any way, provided that
                +   you do at least ONE of the following:
                +
                +   a. place your modifications in the Public Domain or otherwise
                +      make them Freely Available, such as by posting said
                +      modifications to Usenet or an equivalent medium, or by allowing
                +      the author to include your modifications in the software.
                +
                +   b. use the modified software only within your corporation or
                +      organization.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +3. You may distribute the software in object code or binary form,
                +   provided that you do at least ONE of the following:
                +
                +   a. distribute the binaries and library files of the software,
                +      together with instructions (in the manual page or equivalent)
                +      on where to get the original distribution.
                +
                +   b. accompany the distribution with the machine-readable source of
                +      the software.
                +
                +   c. give non-standard binaries non-standard names, with
                +      instructions on where to get the original software distribution.
                +
                +   d. make other distribution arrangements with the author.
                +
                +4. You may modify and include the part of the software into any other
                +   software (possibly commercial).  But some files in the distribution
                +   are not written by the author, so that they are not under these terms.
                +
                +   For the list of those files and their copying conditions, see the
                +   file LEGAL.
                +
                +5. The scripts and library files supplied as input to or produced as
                +   output from the software do not automatically fall under the
                +   copyright of the software, but belong to whomever generated them,
                +   and may be sold commercially, and may be aggregated with this
                +   software.
                +
                +6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
                +   IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
                +   WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
                +   PURPOSE.
        LEGAL
                --- /tmp/20260408-331-1peg1s	2026-04-08 03:06:38.261430047 +0000
                +++ /tmp/d20260408-331-a583yb/base64-0.3.0/LEGAL	2026-04-08 03:06:38.254430021 +0000
                @@ -0,0 +1,60 @@
                +# -*- rdoc -*-
                +
                += LEGAL NOTICE INFORMATION
                +--------------------------
                +
                +All the files in this distribution are covered under either the Ruby's
                +license (see the file COPYING) or public-domain except some files
                +mentioned below.
                +
                +== MIT License
                +>>>
                +      Permission is hereby granted, free of charge, to any person obtaining
                +      a copy of this software and associated documentation files (the
                +      "Software"), to deal in the Software without restriction, including
                +      without limitation the rights to use, copy, modify, merge, publish,
                +      distribute, sublicense, and/or sell copies of the Software, and to
                +      permit persons to whom the Software is furnished to do so, subject to
                +      the following conditions:
                +
                +      The above copyright notice and this permission notice shall be
                +      included in all copies or substantial portions of the Software.
                +
                +      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
                +      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
                +      MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
                +      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
                +      LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
                +      OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
                +      WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
                +
                +== Old-style BSD license
                +>>>
                +      Redistribution and use in source and binary forms, with or without
                +      modification, are permitted provided that the following conditions
                +      are met:
                +      1. Redistributions of source code must retain the above copyright
                +         notice, this list of conditions and the following disclaimer.
                +      2. Redistributions in binary form must reproduce the above copyright
                +         notice, this list of conditions and the following disclaimer in the
                +         documentation and/or other materials provided with the distribution.
                +      3. Neither the name of the University nor the names of its contributors
                +         may be used to endorse or promote products derived from this software
                +         without specific prior written permission.
                +
                +      THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
                +      ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                +      IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
                +      ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
                +      FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                +      DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
                +      OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                +      HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
                +      LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
                +      OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
                +      SUCH DAMAGE.
                +
                +      IMPORTANT NOTE::
                +
                +      From ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change
                +      paragraph 3 above is now null and void.
        sig/base64.rbs
                --- /tmp/20260408-331-8v7g8h	2026-04-08 03:06:38.262430051 +0000
                +++ /tmp/d20260408-331-a583yb/base64-0.3.0/sig/base64.rbs	2026-04-08 03:06:38.255430025 +0000
                @@ -0,0 +1,355 @@
                +# <!-- rdoc-file=lib/base64.rb -->
                +# Module Base64 provides methods for:
                +#
                +# *   Encoding a binary string (containing non-ASCII characters) as a string of
                +#     printable ASCII characters.
                +# *   Decoding such an encoded string.
                +#
                +# Base64 is commonly used in contexts where binary data is not allowed or
                +# supported:
                +#
                +# *   Images in HTML or CSS files, or in URLs.
                +# *   Email attachments.
                +#
                +# A Base64-encoded string is about one-third larger that its source. See the
                +# [Wikipedia article](https://en.wikipedia.org/wiki/Base64) for more
                +# information.
                +#
                +# This module provides three pairs of encode/decode methods. Your choices among
                +# these methods should depend on:
                +#
                +# *   Which character set is to be used for encoding and decoding.
                +# *   Whether "padding" is to be used.
                +# *   Whether encoded strings are to contain newlines.
                +#
                +# Note: Examples on this page assume that the including program has executed:
                +#
                +#     require 'base64'
                +#
                +# ## Encoding Character Sets
                +#
                +# A Base64-encoded string consists only of characters from a 64-character set:
                +#
                +# *   `('A'..'Z')`.
                +# *   `('a'..'z')`.
                +# *   `('0'..'9')`.
                +# *   `=`, the 'padding' character.
                +# *   Either:
                +#     *   `%w[+ /]`:
                +#         [RFC-2045-compliant](https://datatracker.ietf.org/doc/html/rfc2045);
                +#         *not* safe for URLs.
                +#     *   `%w[- _]`:
                +#         [RFC-4648-compliant](https://datatracker.ietf.org/doc/html/rfc4648);
                +#         safe for URLs.
                +#
                +# If you are working with Base64-encoded strings that will come from or be put
                +# into URLs, you should choose this encoder-decoder pair of RFC-4648-compliant
                +# methods:
                +#
                +# *   Base64.urlsafe_encode64 and Base64.urlsafe_decode64.
                +#
                +# Otherwise, you may choose any of the pairs in this module, including the pair
                +# above, or the RFC-2045-compliant pairs:
                +#
                +# *   Base64.encode64 and Base64.decode64.
                +# *   Base64.strict_encode64 and Base64.strict_decode64.
                +#
                +# ## Padding
                +#
                +# Base64-encoding changes a triplet of input bytes into a quartet of output
                +# characters.
                +#
                +# **Padding in Encode Methods**
                +#
                +# Padding -- extending an encoded string with zero, one, or two trailing `=`
                +# characters -- is performed by methods Base64.encode64, Base64.strict_encode64,
                +# and, by default, Base64.urlsafe_encode64:
                +#
                +#     Base64.encode64('s')                         # => "cw==\n"
                +#     Base64.strict_encode64('s')                  # => "cw=="
                +#     Base64.urlsafe_encode64('s')                 # => "cw=="
                +#     Base64.urlsafe_encode64('s', padding: false) # => "cw"
                +#
                +# When padding is performed, the encoded string is always of length *4n*, where
                +# `n` is a non-negative integer:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.strict_encode64('123')      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.strict_encode64('123456')   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate padded output characters of length
                +#     *4(n+1)*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 8 characters.
                +#         Base64.strict_encode64('1234')     # => "MDEyMw=="
                +#         # n = 2:  7 bytes => 12 characters.
                +#         Base64.strict_encode64('1234567')  # => "MDEyMzQ1Ng=="
                +#
                +# *   Input bytes of length *3n+2* generate padded output characters of length
                +#     *4(n+1)*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 8 characters.
                +#         Base64.strict_encode64('12345')    # => "MDEyMzQ="
                +#         # n = 2:  8 bytes => 12 characters.
                +#         Base64.strict_encode64('12345678') # => "MDEyMzQ1Njc="
                +#
                +# When padding is suppressed, for a positive integer *n*:
                +#
                +# *   Input bytes of length *3n* generate unpadded output characters of length
                +#     *4n*:
                +#
                +#         # n = 1:  3 bytes => 4 characters.
                +#         Base64.urlsafe_encode64('123', padding: false)      # => "MDEy"
                +#         # n = 2:  6 bytes => 8 characters.
                +#         Base64.urlsafe_encode64('123456', padding: false)   # => "MDEyMzQ1"
                +#
                +# *   Input bytes of length *3n+1* generate unpadded output characters of length
                +#     *4n+2*, with two padding characters at the end:
                +#
                +#         # n = 1:  4 bytes => 6 characters.
                +#         Base64.urlsafe_encode64('1234', padding: false)     # => "MDEyMw"
                +#         # n = 2:  7 bytes => 10 characters.
                +#         Base64.urlsafe_encode64('1234567', padding: false)  # => "MDEyMzQ1Ng"
                +#
                +# *   Input bytes of length *3n+2* generate unpadded output characters of length
                +#     *4n+3*, with one padding character at the end:
                +#
                +#         # n = 1:  5 bytes => 7 characters.
                +#         Base64.urlsafe_encode64('12345', padding: false)    # => "MDEyMzQ"
                +#         # m = 2:  8 bytes => 11 characters.
                +#         Base64.urlsafe_encode64('12345678', padding: false) # => "MDEyMzQ1Njc"
                +#
                +# **Padding in Decode Methods**
                +#
                +# All of the Base64 decode methods support (but do not require) padding.
                +#
                +# Method Base64.decode64 does not check the size of the padding:
                +#
                +#     Base64.decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +#
                +# Method Base64.strict_decode64 strictly enforces padding size:
                +#
                +#     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +#     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +#     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +#
                +# Method Base64.urlsafe_decode64 allows padding in `str`, which if present, must
                +# be correct: see [Padding](Base64.html#module-Base64-label-Padding), above:
                +#
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +#     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +#
                +# ## Newlines
                +#
                +# An encoded string returned by Base64.encode64 or Base64.urlsafe_encode64 has
                +# an embedded newline character after each 60-character sequence, and, if
                +# non-empty, at the end:
                +#
                +#     # No newline if empty.
                +#     encoded = Base64.encode64("\x00" *  0)
                +#     encoded.index("\n") # => nil
                +#
                +#     # Newline at end of short output.
                +#     encoded = Base64.encode64("\x00" *  1)
                +#     encoded.size        # => 4
                +#     encoded.index("\n") # => 4
                +#
                +#     # Newline at end of longer output.
                +#     encoded = Base64.encode64("\x00" * 45)
                +#     encoded.size        # => 60
                +#     encoded.index("\n") # => 60
                +#
                +#     # Newlines embedded and at end of still longer output.
                +#     encoded = Base64.encode64("\x00" * 46)
                +#     encoded.size                          # => 65
                +#     encoded.rindex("\n")                  # => 65
                +#     encoded.split("\n").map {|s| s.size } # => [60, 4]
                +#
                +# The string to be encoded may itself contain newlines, which are encoded as
                +# Base64:
                +#
                +#       #   Base64.encode64("\n\n\n") # => "CgoK\n"
                +#     s = "This is line 1\nThis is line 2\n"
                +#     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +#
                +module Base64
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #     Base64.decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` are ignored; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.decode64("\x00\n-_") # => ""
                +  #
                +  # Padding in `str` (even if incorrect) is ignored:
                +  #
                +  #     Base64.decode64("MDEyMzQ1Njc")   # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.decode64("MDEyMzQ1Njc==") # => "01234567"
                +  #
                +  def self?.decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string ends with a newline character, and if sufficiently long
                +  # will have one or more embedded newline characters; see
                +  # [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.encode64('*') # => "Kg==\n"
                +  #     Base64.encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq\nKg==\n"
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.encode64("\n\n\n") # => "CgoK\n"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n"
                +  #
                +  def self?.encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_decode64(str)
                +  # -->
                +  # Returns a string containing the decoding of an RFC-2045-compliant
                +  # Base64-encoded string `str`:
                +  #
                +  #     s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #     Base64.strict_decode64(s) # => "This is line 1\nThis is line 2\n"
                +  #
                +  # Non-Base64 characters in `str` not allowed; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above: these
                +  # include newline characters and characters `-` and `/`:
                +  #
                +  #     Base64.strict_decode64("\n") # Raises ArgumentError
                +  #     Base64.strict_decode64('-')  # Raises ArgumentError
                +  #     Base64.strict_decode64('_')  # Raises ArgumentError
                +  #
                +  # Padding in `str`, if present, must be correct:
                +  #
                +  #     Base64.strict_decode64("MDEyMzQ1Njc")   # Raises ArgumentError
                +  #     Base64.strict_decode64("MDEyMzQ1Njc=")  # => "01234567"
                +  #     Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError
                +  #
                +  def self?.strict_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - strict_encode64(bin)
                +  # -->
                +  # Returns a string containing the RFC-2045-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 2045, the returned string may contain the URL-unsafe characters `+` or
                +  # `/`; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.strict_encode64("\xFB\xEF\xBE") # => "++++\n"
                +  #     Base64.strict_encode64("\xFF\xFF\xFF") # => "////\n"
                +  #
                +  # The returned string may include padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding) above.
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg==\n"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.strict_encode64('*') # => "Kg=="
                +  #     Base64.strict_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  # The string to be encoded may itself contain newlines, which will be encoded as
                +  # ordinary Base64:
                +  #
                +  #     Base64.strict_encode64("\n\n\n") # => "CgoK"
                +  #     s = "This is line 1\nThis is line 2\n"
                +  #     Base64.strict_encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK"
                +  #
                +  def self?.strict_encode64: (String bin) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_decode64(str)
                +  # -->
                +  # Returns the decoding of an RFC-4648-compliant Base64-encoded string `str`:
                +  #
                +  # `str` may not contain non-Base64 characters; see [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_decode64('+')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64('/')  # Raises ArgumentError.
                +  #     Base64.urlsafe_decode64("\n") # Raises ArgumentError.
                +  #
                +  # Padding in `str`, if present, must be correct: see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567"
                +  #     Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError.
                +  #
                +  def self?.urlsafe_decode64: (String str) -> String
                +
                +  # <!--
                +  #   rdoc-file=lib/base64.rb
                +  #   - urlsafe_encode64(bin, padding: true)
                +  # -->
                +  # Returns the RFC-4648-compliant Base64-encoding of `bin`.
                +  #
                +  # Per RFC 4648, the returned string will not contain the URL-unsafe characters
                +  # `+` or `/`, but instead may contain the URL-safe characters `-` and `_`; see
                +  # [Encoding Character
                +  # Set](Base64.html#module-Base64-label-Encoding+Character+Sets) above:
                +  #
                +  #     Base64.urlsafe_encode64("\xFB\xEF\xBE") # => "----"
                +  #     Base64.urlsafe_encode64("\xFF\xFF\xFF") # => "____"
                +  #
                +  # By default, the returned string may have padding; see
                +  # [Padding](Base64.html#module-Base64-label-Padding), above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #
                +  # Optionally, you can suppress padding:
                +  #
                +  #     Base64.urlsafe_encode64('*', padding: false) # => "Kg"
                +  #
                +  # The returned string will have no newline characters, regardless of its length;
                +  # see [Newlines](Base64.html#module-Base64-label-Newlines) above:
                +  #
                +  #     Base64.urlsafe_encode64('*') # => "Kg=="
                +  #     Base64.urlsafe_encode64('*' * 46)
                +  #     # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg=="
                +  #
                +  def self?.urlsafe_encode64: (String bin, ?padding: boolish) -> String
                +end
      * Changed:
        lib/base64.rb
                --- /tmp/d20260408-331-a583yb/base64-0.2.0/lib/base64.rb	2026-04-08 03:06:38.252430014 +0000
                +++ /tmp/d20260408-331-a583yb/base64-0.3.0/lib/base64.rb	2026-04-08 03:06:38.254430021 +0000
                @@ -5 +5 @@
                -# - Encoding a binary string (containing non-ASCII characters)
                +# - \Encoding a binary string (containing non-ASCII characters)
                @@ -30 +30 @@
                -# == Encoding Character Sets
                +# == \Encoding Character Sets
                @@ -143 +143 @@
                -# \Method Base64.urlsafe_decode64 allows padding in +str+,
                +# \Method Base64.urlsafe_decode64 allows padding in the encoded string,
                @@ -186 +186 @@
                -  VERSION = "0.2.0"
                +  VERSION = "0.3.0"
                @@ -190 +190,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -222,0 +226,3 @@
                +  # :call-seq:
                +  #   Base64.decode(encoded_string) -> decoded_string
                +  #
                @@ -224 +230 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -229 +235 @@
                -  # Non-\Base64 characters in +str+ are ignored;
                +  # Non-\Base64 characters in +encoded_string+ are ignored;
                @@ -235 +241 @@
                -  # Padding in +str+ (even if incorrect) is ignored:
                +  # Padding in +encoded_string+ (even if incorrect) is ignored:
                @@ -245 +251,4 @@
                -  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.strict_encode64(string) -> encoded_string
                +  #
                +  # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+.
                @@ -276,0 +286,3 @@
                +  # :call-seq:
                +  #   Base64.strict_decode64(encoded_string) -> decoded_string
                +  #
                @@ -278 +290 @@
                -  # \Base64-encoded string +str+:
                +  # \Base64-encoded string +encoded_string+:
                @@ -283 +295 @@
                -  # Non-\Base64 characters in +str+ not allowed;
                +  # Non-\Base64 characters in +encoded_string+ are not allowed;
                @@ -291 +303 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:
                @@ -301 +313,4 @@
                -  # Returns the RFC-4648-compliant \Base64-encoding of +bin+.
                +  # :call-seq:
                +  #   Base64.urlsafe_encode64(string) -> encoded_string
                +  #
                +  # Returns the RFC-4648-compliant \Base64-encoding of +string+.
                @@ -335 +350,4 @@
                -  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +str+:
                +  # :call-seq:
                +  #   Base64.urlsafe_decode64(encoded_string) -> decoded_string
                +  #
                +  # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +encoded_string+:
                @@ -337 +355 @@
                -  # +str+ may not contain non-Base64 characters;
                +  # +encoded_string+ may not contain non-Base64 characters;
                @@ -344 +362 @@
                -  # Padding in +str+, if present, must be correct:
                +  # Padding in +encoded_string+, if present, must be correct:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT date:
    2.1.1: 2025-05-06 00:00:00 UTC
    2.1.2: 1980-01-02 00:00:00 UTC
  DIFFERENT rubygems_version:
    2.1.1: 3.5.22
    2.1.2: 3.6.9
  DIFFERENT version:
    2.1.1: 2.1.1
    2.1.2: 2.1.2
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
            lib/rack/session.rb +0/-1
            lib/rack/session/cookie.rb +4/-2
            lib/rack/session/encryptor.rb +343/-120
            lib/rack/session/version.rb +1/-1
            releases.md +4/-0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
        CHANGELOG.md
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/CHANGELOG.md	2026-04-08 03:06:40.284297816 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/CHANGELOG.md	2026-04-08 03:06:40.291297808 +0000
                @@ -4,0 +5,35 @@
                +## [3.2.6] - 2026-04-01
                +
                +### Security
                +
                +- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
                +- [CVE-2026-34230](https://github.com/advisories/GHSA-v569-hp3g-36wr) Avoid O(n^2) algorithm in `Rack::Utils.select_best_encoding` which could lead to denial of service.
                +- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
                +- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
                +- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
                +- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
                +- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
                +- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
                +- [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) `Rack::Sendfile` header-based `X-Accel-Mapping` regex injection enables unauthorized `X-Accel-Redirect`.
                +- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
                +- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
                +- [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) Quadratic-time multipart header parsing allows denial of service via escape-heavy quoted parameters.
                +- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
                +
                +## [3.2.5] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +### Fixed
                +
                +- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
                +
                +## [3.2.4] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -76,0 +112,13 @@
                +## [3.1.20] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [3.1.19] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -459,0 +508,13 @@
                +
                +## [2.2.22] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [2.2.21] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
        lib/rack/directory.rb
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/lib/rack/directory.rb	2026-04-08 03:06:40.286297814 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/lib/rack/directory.rb	2026-04-08 03:06:40.294297804 +0000
                @@ -20 +20 @@
                -    DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                +    DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                @@ -54 +54 @@
                -        show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
                +        show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
                @@ -84,0 +85 @@
                +      @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
                @@ -121 +122,3 @@
                -      return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
                +
                +      expanded_path = ::File.expand_path(::File.join(@root, path_info))
                +      return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
        lib/rack/files.rb
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/lib/rack/files.rb	2026-04-08 03:06:40.287297812 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/lib/rack/files.rb	2026-04-08 03:06:40.294297804 +0000
                @@ -197 +197 @@
                -          CONTENT_LENGTH => body.size.to_s,
                +          CONTENT_LENGTH => body.bytesize.to_s,
        lib/rack/mock_response.rb
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/lib/rack/mock_response.rb	2026-04-08 03:06:40.288297811 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/lib/rack/mock_response.rb	2026-04-08 03:06:40.295297803 +0000
                @@ -2,0 +3 @@
                +require 'stringio'
                @@ -85,2 +86,10 @@
                -      @body.each do |chunk|
                -        buffer << chunk
                +      begin
                +        if @body.respond_to?(:each)
                +          @body.each do |chunk|
                +            buffer << chunk
                +          end
                +        else
                +          @body.call(StringIO.new(buffer))
                +        end
                +      ensure
                +        @body.close if @body.respond_to?(:close)
        lib/rack/multipart/parser.rb
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/lib/rack/multipart/parser.rb	2026-04-08 03:06:40.289297810 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/lib/rack/multipart/parser.rb	2026-04-08 03:06:40.296297802 +0000
                @@ -36 +36 @@
                -    MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
                +    MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
                @@ -82,0 +83,7 @@
                +      bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
                +      PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
                +      private_constant :PARSER_BYTESIZE_LIMIT
                +
                +      CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
                +      private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +
                @@ -119 +126,9 @@
                -        data[1]
                +
                +        unless data[1].empty?
                +          raise Error, "whitespace between boundary parameter name and equal sign"
                +        end
                +        if data.post_match.match?(/boundary\s*=/i)
                +          raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
                +        end
                +
                +        data[2]
                @@ -127,0 +143,4 @@
                +        if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
                +          raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +        end
                +
                @@ -243,0 +263,2 @@
                +        @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
                +        @content_disposition_quoted_escapes = 0
                @@ -254,0 +276 @@
                +        @total_bytes_read &&= nil if io.is_a?(BoundedIO)
                @@ -292,0 +315,6 @@
                +        if @total_bytes_read
                +          @total_bytes_read += content.bytesize
                +          if @total_bytes_read > PARSER_BYTESIZE_LIMIT
                +            raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +          end
                +        end
                @@ -341,0 +370,3 @@
                +      OBS_UNFOLD = /\r\n([ \t])/
                +      private_constant :OBS_UNFOLD
                +
                @@ -345,0 +377,2 @@
                +          content_type.gsub!(OBS_UNFOLD, '\1') if content_type
                +
                @@ -348,0 +382,3 @@
                +            # Implement OBS unfolding (RFC 5322 Section 2.2.3)
                +            disposition.gsub!(OBS_UNFOLD, '\1')
                +
                @@ -385,0 +422,5 @@
                +                  @content_disposition_quoted_escapes += 1
                +                  if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +                    raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
                +                  end
                +
                @@ -454 +495 @@
                -          raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
                +          raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
        lib/rack/request.rb
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/lib/rack/request.rb	2026-04-08 03:06:40.290297809 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/lib/rack/request.rb	2026-04-08 03:06:40.297297801 +0000
                @@ -726,2 +726,2 @@
                -          # Match any other printable string (except square brackets) as a hostname
                -          (?<address>[[[:graph:]&&[^\[\]]]]*?)
                +          # Match characters allowed by RFC 3986 Section 3.2.2
                +          (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
        lib/rack/sendfile.rb
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/lib/rack/sendfile.rb	2026-04-08 03:06:40.290297809 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/lib/rack/sendfile.rb	2026-04-08 03:06:40.297297801 +0000
                @@ -54 +54 @@
                -  # that it maps to. The middleware performs a simple substitution on the
                +  # that it maps to. The middleware performs a case-insensitive substitution on the
                @@ -189 +189 @@
                -          new_path = path.sub(/\A#{internal}/i, external)
                +          new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
        lib/rack/static.rb
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/lib/rack/static.rb	2026-04-08 03:06:40.290297809 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/lib/rack/static.rb	2026-04-08 03:06:40.297297801 +0000
                @@ -95,0 +96,3 @@
                +      if @urls.kind_of?(Array)
                +        @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
                +      end
                @@ -118 +121 @@
                -      @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
                +      @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
                @@ -167,0 +171,2 @@
                +      path = ::Rack::Utils.unescape_path(path)
                +
                @@ -175 +179,0 @@
                -          path = ::Rack::Utils.unescape(path)
                @@ -178 +182 @@
                -          /\.(#{rule.join('|')})\z/.match?(path)
                +          /\.#{Regexp.union(rule)}\z/.match?(path)
        lib/rack/utils.rb
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/lib/rack/utils.rb	2026-04-08 03:06:40.291297808 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/lib/rack/utils.rb	2026-04-08 03:06:40.298297799 +0000
                @@ -149,3 +149,2 @@
                -    def forwarded_values(forwarded_header)
                -      return nil unless forwarded_header
                -      forwarded_header = forwarded_header.to_s.gsub("\n", ";")
                +    ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
                +    private_constant :ALLOWED_FORWARED_PARAMS
                @@ -153,5 +152,59 @@
                -      forwarded_header.split(';').each_with_object({}) do |field, values|
                -        field.split(',').each do |pair|
                -          pair = pair.split('=').map(&:strip).join('=')
                -          return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
                -          (values[$1.downcase.to_sym] ||= []) << $2
                +    def forwarded_values(forwarded_header)
                +      return unless forwarded_header
                +      header = forwarded_header.to_s.tr("\n", ";")
                +      header.sub!(/\A[\s;,]+/, '')
                +      num_params = num_escapes = 0
                +      max_params = max_escapes = 1024
                +      params = {}
                +
                +      # Parse parameter list
                +      while i = header.index('=')
                +        # Only parse up to max parameters, to avoid potential denial of service
                +        num_params += 1
                +        return if num_params > max_params
                +
                +        # Found end of parameter name, ensure forward progress in loop
                +        param = header.slice!(0, i+1)
                +
                +        # Remove ending equals and preceding whitespace from parameter name
                +        param.chomp!('=')
                +        param.strip!
                +        param.downcase!
                +        return unless param = ALLOWED_FORWARED_PARAMS[param]
                +
                +        if header[0] == '"'
                +          # Parameter value is quoted, parse it, handling backslash escapes
                +          header.slice!(0, 1)
                +          value = String.new
                +
                +          while i = header.index(/(["\\])/)
                +            c = $1
                +
                +            # Append all content until ending quote or escape
                +            value << header.slice!(0, i)
                +
                +            # Remove either backslash or ending quote,
                +            # ensures forward progress in loop
                +            header.slice!(0, 1)
                +
                +            # stop parsing parameter value if found ending quote
                +            break if c == '"'
                +
                +            # Only allow up to max escapes, to avoid potential denial of service
                +            num_escapes += 1
                +            return if num_escapes > max_escapes
                +            escaped_char = header.slice!(0, 1)
                +            value << escaped_char
                +          end
                +        else
                +          if i = header.index(/[;,]/)
                +            # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
                +            value = header.slice!(0, i)
                +            value.sub!(/[\s;,]+\z/, '')
                +          else
                +            # If no ending semicolon, assume remainder of line is value and stop parsing
                +            header.strip!
                +            value = header
                +            header = ''
                +          end
                +          value.lstrip!
                @@ -158,0 +212,5 @@
                +
                +        (params[param] ||= []) << value
                +
                +        # skip trailing semicolons/commas/whitespace, to proceed to next parameter
                +        header.sub!(/\A[\s;,]+/, '') unless header.empty?
                @@ -159,0 +218,2 @@
                +
                +      params
                @@ -195,0 +256,18 @@
                +    # Given an array of available encoding strings, and an array of
                +    # acceptable encodings for a request, where each element of the
                +    # acceptable encodings array is an array where the first element
                +    # is an encoding name and the second element is the numeric
                +    # priority for the encoding, return the available encoding with
                +    # the highest priority.
                +    #
                +    # The accept_encoding argument is typically generated by calling
                +    # Request#accept_encoding.
                +    #
                +    # Example:
                +    #
                +    #   select_best_encoding(%w(compress gzip identity),
                +    #                        [["compress", 0.5], ["gzip", 1.0]])
                +    #   # => "gzip"
                +    #
                +    # To reduce denial of service potential, only the first 16
                +    # acceptable encodings are considered.
                @@ -198,0 +277,2 @@
                +      # Only process the first 16 encodings
                +      accept_encoding = accept_encoding[0...16]
                @@ -199,0 +280 @@
                +      wildcard_seen = false
                @@ -205,2 +286,5 @@
                -          (available_encodings - accept_encoding.map(&:first)).each do |m2|
                -            expanded_accept_encoding << [m2, q, preference]
                +          unless wildcard_seen
                +            (available_encodings - accept_encoding.map(&:first)).each do |m2|
                +              expanded_accept_encoding << [m2, q, preference]
                +            end
                +            wildcard_seen = true
                @@ -214 +298,7 @@
                -        .sort_by { |_, q, p| [-q, p] }
                +        .sort do |(_, q1, p1), (_, q2, p2)|
                +          if r = (q1 <=> q2).nonzero?
                +            -r
                +          else
                +            (p1 <=> p2).nonzero? || 0
                +          end
                +        end
                @@ -402,2 +492,2 @@
                -    def byte_ranges(env, size)
                -      get_byte_ranges env['HTTP_RANGE'], size
                +    def byte_ranges(env, size, max_ranges: 100)
                +      get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
                @@ -406 +496 @@
                -    def get_byte_ranges(http_range, size)
                +    def get_byte_ranges(http_range, size, max_ranges: 100)
                @@ -410,0 +501,2 @@
                +      byte_range = $1
                +      return nil if byte_range.count(',') >= max_ranges
                @@ -412 +504 @@
                -      $1.split(/,[ \t]*/).each do |range_spec|
                +      byte_range.split(/,[ \t]*/).each do |range_spec|
        lib/rack/version.rb
                --- /tmp/d20260408-646-o9t0c5/rack-3.2.3/lib/rack/version.rb	2026-04-08 03:06:40.291297808 +0000
                +++ /tmp/d20260408-646-o9t0c5/rack-3.2.6/lib/rack/version.rb	2026-04-08 03:06:40.298297799 +0000
                @@ -9 +9 @@
                -  VERSION = "3.2.3"
                +  VERSION = "3.2.6"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
        lib/rack/session.rb
                --- /tmp/d20260408-931-i5kj39/rack-session-2.1.1/lib/rack/session.rb	2026-04-08 03:06:52.273268443 +0000
                +++ /tmp/d20260408-931-i5kj39/rack-session-2.1.2/lib/rack/session.rb	2026-04-08 03:06:52.276268427 +0000
                @@ -11 +10,0 @@
                -    autoload :Memcache, "rack/session/memcache"
        lib/rack/session/cookie.rb
                --- /tmp/d20260408-931-i5kj39/rack-session-2.1.1/lib/rack/session/cookie.rb	2026-04-08 03:06:52.274268437 +0000
                +++ /tmp/d20260408-931-i5kj39/rack-session-2.1.2/lib/rack/session/cookie.rb	2026-04-08 03:06:52.276268427 +0000
                @@ -240,2 +240,4 @@
                -            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.
        lib/rack/session/encryptor.rb
                --- /tmp/d20260408-931-i5kj39/rack-session-2.1.1/lib/rack/session/encryptor.rb	2026-04-08 03:06:52.274268437 +0000
                +++ /tmp/d20260408-931-i5kj39/rack-session-2.1.2/lib/rack/session/encryptor.rb	2026-04-08 03:06:52.276268427 +0000
                @@ -7,0 +8 @@
                +require 'json'
                @@ -10 +10,0 @@
                -require 'zlib'
                @@ -26,30 +26,35 @@
                -      # 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
                @@ -57,2 +62,33 @@
                -        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
                @@ -60,4 +96,15 @@
                -        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
                @@ -66,3 +113,2 @@
                -        @options = {
                -          serialize_json: false, pad_size: 32, purpose: nil
                -        }.update(opts)
                +        def decrypt(base64_data)
                +          data = Base64.urlsafe_decode64(base64_data)
                @@ -70,2 +116,2 @@
                -        @hmac_secret = secret.dup.force_encoding('BINARY')
                -        @cipher_secret = @hmac_secret.slice!(0, 32)
                +          signature = data.slice!(-32..-1)
                +          verify_authenticity!(data, signature)
                @@ -73,3 +119,2 @@
                -        @hmac_secret.freeze
                -        @cipher_secret.freeze
                -      end
                +          version = data.slice!(0, 1)
                +          raise InvalidMessage, 'wrong version' unless version == "\1"
                @@ -77,2 +122,2 @@
                -      def decrypt(base64_data)
                -        data = Base64.urlsafe_decode64(base64_data)
                +          message_secret = data.slice!(0, 32)
                +          cipher_iv = data.slice!(0, 16)
                @@ -80 +125,2 @@
                -        signature = data.slice!(-32..-1)
                +          cipher = new_cipher
                +          cipher.decrypt
                @@ -82 +128 @@
                -        verify_authenticity! data, signature
                +          set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                @@ -84,4 +130,2 @@
                -        # 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
                @@ -89,2 +133,4 @@
                -        cipher = new_cipher
                -        cipher.decrypt
                +          deserialized_message data
                +        rescue ArgumentError
                +          raise InvalidSignature, 'Message invalid'
                +        end
                @@ -92 +138,2 @@
                -        set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                +        def encrypt(message)
                +          version = "\1"
                @@ -94,2 +141,2 @@
                -        cipher.iv = cipher_iv
                -        data = cipher.update(data) << cipher.final
                +          serialized_payload = serialize_payload(message)
                +          message_secret, cipher_secret = new_message_and_cipher_secret
                @@ -97,4 +144,2 @@
                -        deserialized_message data
                -      rescue ArgumentError
                -        raise InvalidSignature, 'Message invalid'
                -      end
                +          cipher = new_cipher
                +          cipher.encrypt
                @@ -102,2 +147 @@
                -      def encrypt(message)
                -        version = "\1"
                +          set_cipher_key(cipher, cipher_secret)
                @@ -105,2 +149 @@
                -        serialized_payload = serialize_payload(message)
                -        message_secret, cipher_secret = new_message_and_cipher_secret
                +          cipher_iv = cipher.random_iv
                @@ -108,2 +151 @@
                -        cipher = new_cipher
                -        cipher.encrypt
                +          encrypted_data = cipher.update(serialized_payload) << cipher.final
                @@ -111 +153,6 @@
                -        set_cipher_key(cipher, cipher_secret)
                +          data = String.new
                +          data << version
                +          data << message_secret
                +          data << cipher_iv
                +          data << encrypted_data
                +          data << compute_signature(data)
                @@ -113 +160,2 @@
                -        cipher_iv = cipher.random_iv
                +          Base64.urlsafe_encode64(data)
                +        end
                @@ -115 +163 @@
                -        encrypted_data = cipher.update(serialized_payload) << cipher.final
                +        private
                @@ -117,6 +165,3 @@
                -        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
                @@ -124,2 +169,2 @@
                -        Base64.urlsafe_encode64(data)
                -      end
                +        def new_message_and_cipher_secret
                +          message_secret = SecureRandom.random_bytes(32)
                @@ -127 +172,2 @@
                -      private
                +          [message_secret, cipher_secret_from_message_secret(message_secret)]
                +        end
                @@ -129,3 +175,3 @@
                -      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
                @@ -133,2 +179,3 @@
                -      def new_message_and_cipher_secret
                -        message_secret = SecureRandom.random_bytes(32)
                +        def set_cipher_key(cipher, key)
                +          cipher.key = key
                +        end
                @@ -136,2 +183,3 @@
                -        [message_secret, cipher_secret_from_message_secret(message_secret)]
                -      end
                +        def compute_signature(data)
                +          signing_data = data
                +          signing_data += @options[:purpose] if @options[:purpose]
                @@ -139,3 +187,2 @@
                -      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
                @@ -143,3 +190,2 @@
                -      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?
                @@ -147,2 +193,4 @@
                -      def serializer
                -        @serializer ||= @options[:serialize_json] ? JSON : Marshal
                +          unless Rack::Utils.secure_compare(signature, compute_signature(data))
                +            raise InvalidSignature, 'HMAC is invalid'
                +          end
                +        end
                @@ -151,3 +199,40 @@
                -      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
                @@ -155 +240,105 @@
                -        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
                @@ -158,2 +347,2 @@
                -      def verify_authenticity!(data, signature)
                -        raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
                +      def initialize(secret, opts = {})
                +        opts = opts.dup
                @@ -161,2 +350,9 @@
                -        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)
                @@ -166,5 +362,10 @@
                -      # 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
                @@ -172 +373,2 @@
                -        return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
                +        decryptor.decrypt(base64_data)
                +      end
                @@ -174,2 +376,8 @@
                -        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
                @@ -177 +385 @@
                -        "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
                +        encryptor.encrypt(message)
                @@ -180,5 +388,3 @@
                -      # 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
                @@ -186,3 +392,20 @@
                -        # 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'
        lib/rack/session/version.rb
                --- /tmp/d20260408-931-i5kj39/rack-session-2.1.1/lib/rack/session/version.rb	2026-04-08 03:06:52.274268437 +0000
                +++ /tmp/d20260408-931-i5kj39/rack-session-2.1.2/lib/rack/session/version.rb	2026-04-08 03:06:52.276268427 +0000
                @@ -8 +8 @@
                -    VERSION = "2.1.1"
                +    VERSION = "2.1.2"
        releases.md
                --- /tmp/d20260408-931-i5kj39/rack-session-2.1.1/releases.md	2026-04-08 03:06:52.274268437 +0000
                +++ /tmp/d20260408-931-i5kj39/rack-session-2.1.2/releases.md	2026-04-08 03:06:52.277268421 +0000
                @@ -2,0 +3,4 @@
                +## 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.
                +

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT date:
    2.1.1: 2025-05-06 00:00:00 UTC
    2.1.2: 1980-01-02 00:00:00 UTC
  DIFFERENT rubygems_version:
    2.1.1: 3.5.22
    2.1.2: 3.6.9
  DIFFERENT version:
    2.1.1: 2.1.1
    2.1.2: 2.1.2
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
            lib/rack/session.rb +0/-1
            lib/rack/session/cookie.rb +4/-2
            lib/rack/session/encryptor.rb +343/-120
            lib/rack/session/version.rb +1/-1
            releases.md +4/-0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
        CHANGELOG.md
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/CHANGELOG.md	2026-04-08 03:06:59.454513765 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/CHANGELOG.md	2026-04-08 03:06:59.466513686 +0000
                @@ -4,0 +5,35 @@
                +## [3.2.6] - 2026-04-01
                +
                +### Security
                +
                +- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
                +- [CVE-2026-34230](https://github.com/advisories/GHSA-v569-hp3g-36wr) Avoid O(n^2) algorithm in `Rack::Utils.select_best_encoding` which could lead to denial of service.
                +- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
                +- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
                +- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
                +- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
                +- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
                +- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
                +- [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) `Rack::Sendfile` header-based `X-Accel-Mapping` regex injection enables unauthorized `X-Accel-Redirect`.
                +- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
                +- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
                +- [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) Quadratic-time multipart header parsing allows denial of service via escape-heavy quoted parameters.
                +- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
                +
                +## [3.2.5] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +### Fixed
                +
                +- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
                +
                +## [3.2.4] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -76,0 +112,13 @@
                +## [3.1.20] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [3.1.19] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -459,0 +508,13 @@
                +
                +## [2.2.22] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [2.2.21] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
        lib/rack/directory.rb
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/lib/rack/directory.rb	2026-04-08 03:06:59.461513719 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/lib/rack/directory.rb	2026-04-08 03:06:59.468513673 +0000
                @@ -20 +20 @@
                -    DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                +    DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                @@ -54 +54 @@
                -        show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
                +        show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
                @@ -84,0 +85 @@
                +      @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
                @@ -121 +122,3 @@
                -      return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
                +
                +      expanded_path = ::File.expand_path(::File.join(@root, path_info))
                +      return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
        lib/rack/files.rb
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/lib/rack/files.rb	2026-04-08 03:06:59.461513719 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/lib/rack/files.rb	2026-04-08 03:06:59.469513666 +0000
                @@ -197 +197 @@
                -          CONTENT_LENGTH => body.size.to_s,
                +          CONTENT_LENGTH => body.bytesize.to_s,
        lib/rack/mock_response.rb
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/lib/rack/mock_response.rb	2026-04-08 03:06:59.463513706 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/lib/rack/mock_response.rb	2026-04-08 03:06:59.470513660 +0000
                @@ -2,0 +3 @@
                +require 'stringio'
                @@ -85,2 +86,10 @@
                -      @body.each do |chunk|
                -        buffer << chunk
                +      begin
                +        if @body.respond_to?(:each)
                +          @body.each do |chunk|
                +            buffer << chunk
                +          end
                +        else
                +          @body.call(StringIO.new(buffer))
                +        end
                +      ensure
                +        @body.close if @body.respond_to?(:close)
        lib/rack/multipart/parser.rb
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/lib/rack/multipart/parser.rb	2026-04-08 03:06:59.463513706 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/lib/rack/multipart/parser.rb	2026-04-08 03:06:59.470513660 +0000
                @@ -36 +36 @@
                -    MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
                +    MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
                @@ -82,0 +83,7 @@
                +      bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
                +      PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
                +      private_constant :PARSER_BYTESIZE_LIMIT
                +
                +      CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
                +      private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +
                @@ -119 +126,9 @@
                -        data[1]
                +
                +        unless data[1].empty?
                +          raise Error, "whitespace between boundary parameter name and equal sign"
                +        end
                +        if data.post_match.match?(/boundary\s*=/i)
                +          raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
                +        end
                +
                +        data[2]
                @@ -127,0 +143,4 @@
                +        if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
                +          raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +        end
                +
                @@ -243,0 +263,2 @@
                +        @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
                +        @content_disposition_quoted_escapes = 0
                @@ -254,0 +276 @@
                +        @total_bytes_read &&= nil if io.is_a?(BoundedIO)
                @@ -292,0 +315,6 @@
                +        if @total_bytes_read
                +          @total_bytes_read += content.bytesize
                +          if @total_bytes_read > PARSER_BYTESIZE_LIMIT
                +            raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +          end
                +        end
                @@ -341,0 +370,3 @@
                +      OBS_UNFOLD = /\r\n([ \t])/
                +      private_constant :OBS_UNFOLD
                +
                @@ -345,0 +377,2 @@
                +          content_type.gsub!(OBS_UNFOLD, '\1') if content_type
                +
                @@ -348,0 +382,3 @@
                +            # Implement OBS unfolding (RFC 5322 Section 2.2.3)
                +            disposition.gsub!(OBS_UNFOLD, '\1')
                +
                @@ -385,0 +422,5 @@
                +                  @content_disposition_quoted_escapes += 1
                +                  if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +                    raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
                +                  end
                +
                @@ -454 +495 @@
                -          raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
                +          raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
        lib/rack/request.rb
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/lib/rack/request.rb	2026-04-08 03:06:59.464513699 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/lib/rack/request.rb	2026-04-08 03:06:59.471513653 +0000
                @@ -726,2 +726,2 @@
                -          # Match any other printable string (except square brackets) as a hostname
                -          (?<address>[[[:graph:]&&[^\[\]]]]*?)
                +          # Match characters allowed by RFC 3986 Section 3.2.2
                +          (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
        lib/rack/sendfile.rb
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/lib/rack/sendfile.rb	2026-04-08 03:06:59.465513693 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/lib/rack/sendfile.rb	2026-04-08 03:06:59.471513653 +0000
                @@ -54 +54 @@
                -  # that it maps to. The middleware performs a simple substitution on the
                +  # that it maps to. The middleware performs a case-insensitive substitution on the
                @@ -189 +189 @@
                -          new_path = path.sub(/\A#{internal}/i, external)
                +          new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
        lib/rack/static.rb
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/lib/rack/static.rb	2026-04-08 03:06:59.465513693 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/lib/rack/static.rb	2026-04-08 03:06:59.472513647 +0000
                @@ -95,0 +96,3 @@
                +      if @urls.kind_of?(Array)
                +        @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
                +      end
                @@ -118 +121 @@
                -      @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
                +      @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
                @@ -167,0 +171,2 @@
                +      path = ::Rack::Utils.unescape_path(path)
                +
                @@ -175 +179,0 @@
                -          path = ::Rack::Utils.unescape(path)
                @@ -178 +182 @@
                -          /\.(#{rule.join('|')})\z/.match?(path)
                +          /\.#{Regexp.union(rule)}\z/.match?(path)
        lib/rack/utils.rb
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/lib/rack/utils.rb	2026-04-08 03:06:59.465513693 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/lib/rack/utils.rb	2026-04-08 03:06:59.472513647 +0000
                @@ -149,3 +149,2 @@
                -    def forwarded_values(forwarded_header)
                -      return nil unless forwarded_header
                -      forwarded_header = forwarded_header.to_s.gsub("\n", ";")
                +    ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
                +    private_constant :ALLOWED_FORWARED_PARAMS
                @@ -153,5 +152,59 @@
                -      forwarded_header.split(';').each_with_object({}) do |field, values|
                -        field.split(',').each do |pair|
                -          pair = pair.split('=').map(&:strip).join('=')
                -          return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
                -          (values[$1.downcase.to_sym] ||= []) << $2
                +    def forwarded_values(forwarded_header)
                +      return unless forwarded_header
                +      header = forwarded_header.to_s.tr("\n", ";")
                +      header.sub!(/\A[\s;,]+/, '')
                +      num_params = num_escapes = 0
                +      max_params = max_escapes = 1024
                +      params = {}
                +
                +      # Parse parameter list
                +      while i = header.index('=')
                +        # Only parse up to max parameters, to avoid potential denial of service
                +        num_params += 1
                +        return if num_params > max_params
                +
                +        # Found end of parameter name, ensure forward progress in loop
                +        param = header.slice!(0, i+1)
                +
                +        # Remove ending equals and preceding whitespace from parameter name
                +        param.chomp!('=')
                +        param.strip!
                +        param.downcase!
                +        return unless param = ALLOWED_FORWARED_PARAMS[param]
                +
                +        if header[0] == '"'
                +          # Parameter value is quoted, parse it, handling backslash escapes
                +          header.slice!(0, 1)
                +          value = String.new
                +
                +          while i = header.index(/(["\\])/)
                +            c = $1
                +
                +            # Append all content until ending quote or escape
                +            value << header.slice!(0, i)
                +
                +            # Remove either backslash or ending quote,
                +            # ensures forward progress in loop
                +            header.slice!(0, 1)
                +
                +            # stop parsing parameter value if found ending quote
                +            break if c == '"'
                +
                +            # Only allow up to max escapes, to avoid potential denial of service
                +            num_escapes += 1
                +            return if num_escapes > max_escapes
                +            escaped_char = header.slice!(0, 1)
                +            value << escaped_char
                +          end
                +        else
                +          if i = header.index(/[;,]/)
                +            # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
                +            value = header.slice!(0, i)
                +            value.sub!(/[\s;,]+\z/, '')
                +          else
                +            # If no ending semicolon, assume remainder of line is value and stop parsing
                +            header.strip!
                +            value = header
                +            header = ''
                +          end
                +          value.lstrip!
                @@ -158,0 +212,5 @@
                +
                +        (params[param] ||= []) << value
                +
                +        # skip trailing semicolons/commas/whitespace, to proceed to next parameter
                +        header.sub!(/\A[\s;,]+/, '') unless header.empty?
                @@ -159,0 +218,2 @@
                +
                +      params
                @@ -195,0 +256,18 @@
                +    # Given an array of available encoding strings, and an array of
                +    # acceptable encodings for a request, where each element of the
                +    # acceptable encodings array is an array where the first element
                +    # is an encoding name and the second element is the numeric
                +    # priority for the encoding, return the available encoding with
                +    # the highest priority.
                +    #
                +    # The accept_encoding argument is typically generated by calling
                +    # Request#accept_encoding.
                +    #
                +    # Example:
                +    #
                +    #   select_best_encoding(%w(compress gzip identity),
                +    #                        [["compress", 0.5], ["gzip", 1.0]])
                +    #   # => "gzip"
                +    #
                +    # To reduce denial of service potential, only the first 16
                +    # acceptable encodings are considered.
                @@ -198,0 +277,2 @@
                +      # Only process the first 16 encodings
                +      accept_encoding = accept_encoding[0...16]
                @@ -199,0 +280 @@
                +      wildcard_seen = false
                @@ -205,2 +286,5 @@
                -          (available_encodings - accept_encoding.map(&:first)).each do |m2|
                -            expanded_accept_encoding << [m2, q, preference]
                +          unless wildcard_seen
                +            (available_encodings - accept_encoding.map(&:first)).each do |m2|
                +              expanded_accept_encoding << [m2, q, preference]
                +            end
                +            wildcard_seen = true
                @@ -214 +298,7 @@
                -        .sort_by { |_, q, p| [-q, p] }
                +        .sort do |(_, q1, p1), (_, q2, p2)|
                +          if r = (q1 <=> q2).nonzero?
                +            -r
                +          else
                +            (p1 <=> p2).nonzero? || 0
                +          end
                +        end
                @@ -402,2 +492,2 @@
                -    def byte_ranges(env, size)
                -      get_byte_ranges env['HTTP_RANGE'], size
                +    def byte_ranges(env, size, max_ranges: 100)
                +      get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
                @@ -406 +496 @@
                -    def get_byte_ranges(http_range, size)
                +    def get_byte_ranges(http_range, size, max_ranges: 100)
                @@ -410,0 +501,2 @@
                +      byte_range = $1
                +      return nil if byte_range.count(',') >= max_ranges
                @@ -412 +504 @@
                -      $1.split(/,[ \t]*/).each do |range_spec|
                +      byte_range.split(/,[ \t]*/).each do |range_spec|
        lib/rack/version.rb
                --- /tmp/d20260408-662-yue4n0/rack-3.2.3/lib/rack/version.rb	2026-04-08 03:06:59.465513693 +0000
                +++ /tmp/d20260408-662-yue4n0/rack-3.2.6/lib/rack/version.rb	2026-04-08 03:06:59.472513647 +0000
                @@ -9 +9 @@
                -  VERSION = "3.2.3"
                +  VERSION = "3.2.6"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
        lib/rack/session.rb
                --- /tmp/d20260408-951-t1jzsn/rack-session-2.1.1/lib/rack/session.rb	2026-04-08 03:07:04.394270710 +0000
                +++ /tmp/d20260408-951-t1jzsn/rack-session-2.1.2/lib/rack/session.rb	2026-04-08 03:07:04.395270709 +0000
                @@ -11 +10,0 @@
                -    autoload :Memcache, "rack/session/memcache"
        lib/rack/session/cookie.rb
                --- /tmp/d20260408-951-t1jzsn/rack-session-2.1.1/lib/rack/session/cookie.rb	2026-04-08 03:07:04.394270710 +0000
                +++ /tmp/d20260408-951-t1jzsn/rack-session-2.1.2/lib/rack/session/cookie.rb	2026-04-08 03:07:04.396270707 +0000
                @@ -240,2 +240,4 @@
                -            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.
        lib/rack/session/encryptor.rb
                --- /tmp/d20260408-951-t1jzsn/rack-session-2.1.1/lib/rack/session/encryptor.rb	2026-04-08 03:07:04.395270709 +0000
                +++ /tmp/d20260408-951-t1jzsn/rack-session-2.1.2/lib/rack/session/encryptor.rb	2026-04-08 03:07:04.396270707 +0000
                @@ -7,0 +8 @@
                +require 'json'
                @@ -10 +10,0 @@
                -require 'zlib'
                @@ -26,30 +26,35 @@
                -      # 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
                @@ -57,2 +62,33 @@
                -        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
                @@ -60,4 +96,15 @@
                -        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
                @@ -66,3 +113,2 @@
                -        @options = {
                -          serialize_json: false, pad_size: 32, purpose: nil
                -        }.update(opts)
                +        def decrypt(base64_data)
                +          data = Base64.urlsafe_decode64(base64_data)
                @@ -70,2 +116,2 @@
                -        @hmac_secret = secret.dup.force_encoding('BINARY')
                -        @cipher_secret = @hmac_secret.slice!(0, 32)
                +          signature = data.slice!(-32..-1)
                +          verify_authenticity!(data, signature)
                @@ -73,3 +119,2 @@
                -        @hmac_secret.freeze
                -        @cipher_secret.freeze
                -      end
                +          version = data.slice!(0, 1)
                +          raise InvalidMessage, 'wrong version' unless version == "\1"
                @@ -77,2 +122,2 @@
                -      def decrypt(base64_data)
                -        data = Base64.urlsafe_decode64(base64_data)
                +          message_secret = data.slice!(0, 32)
                +          cipher_iv = data.slice!(0, 16)
                @@ -80 +125,2 @@
                -        signature = data.slice!(-32..-1)
                +          cipher = new_cipher
                +          cipher.decrypt
                @@ -82 +128 @@
                -        verify_authenticity! data, signature
                +          set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                @@ -84,4 +130,2 @@
                -        # 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
                @@ -89,2 +133,4 @@
                -        cipher = new_cipher
                -        cipher.decrypt
                +          deserialized_message data
                +        rescue ArgumentError
                +          raise InvalidSignature, 'Message invalid'
                +        end
                @@ -92 +138,2 @@
                -        set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                +        def encrypt(message)
                +          version = "\1"
                @@ -94,2 +141,2 @@
                -        cipher.iv = cipher_iv
                -        data = cipher.update(data) << cipher.final
                +          serialized_payload = serialize_payload(message)
                +          message_secret, cipher_secret = new_message_and_cipher_secret
                @@ -97,4 +144,2 @@
                -        deserialized_message data
                -      rescue ArgumentError
                -        raise InvalidSignature, 'Message invalid'
                -      end
                +          cipher = new_cipher
                +          cipher.encrypt
                @@ -102,2 +147 @@
                -      def encrypt(message)
                -        version = "\1"
                +          set_cipher_key(cipher, cipher_secret)
                @@ -105,2 +149 @@
                -        serialized_payload = serialize_payload(message)
                -        message_secret, cipher_secret = new_message_and_cipher_secret
                +          cipher_iv = cipher.random_iv
                @@ -108,2 +151 @@
                -        cipher = new_cipher
                -        cipher.encrypt
                +          encrypted_data = cipher.update(serialized_payload) << cipher.final
                @@ -111 +153,6 @@
                -        set_cipher_key(cipher, cipher_secret)
                +          data = String.new
                +          data << version
                +          data << message_secret
                +          data << cipher_iv
                +          data << encrypted_data
                +          data << compute_signature(data)
                @@ -113 +160,2 @@
                -        cipher_iv = cipher.random_iv
                +          Base64.urlsafe_encode64(data)
                +        end
                @@ -115 +163 @@
                -        encrypted_data = cipher.update(serialized_payload) << cipher.final
                +        private
                @@ -117,6 +165,3 @@
                -        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
                @@ -124,2 +169,2 @@
                -        Base64.urlsafe_encode64(data)
                -      end
                +        def new_message_and_cipher_secret
                +          message_secret = SecureRandom.random_bytes(32)
                @@ -127 +172,2 @@
                -      private
                +          [message_secret, cipher_secret_from_message_secret(message_secret)]
                +        end
                @@ -129,3 +175,3 @@
                -      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
                @@ -133,2 +179,3 @@
                -      def new_message_and_cipher_secret
                -        message_secret = SecureRandom.random_bytes(32)
                +        def set_cipher_key(cipher, key)
                +          cipher.key = key
                +        end
                @@ -136,2 +183,3 @@
                -        [message_secret, cipher_secret_from_message_secret(message_secret)]
                -      end
                +        def compute_signature(data)
                +          signing_data = data
                +          signing_data += @options[:purpose] if @options[:purpose]
                @@ -139,3 +187,2 @@
                -      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
                @@ -143,3 +190,2 @@
                -      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?
                @@ -147,2 +193,4 @@
                -      def serializer
                -        @serializer ||= @options[:serialize_json] ? JSON : Marshal
                +          unless Rack::Utils.secure_compare(signature, compute_signature(data))
                +            raise InvalidSignature, 'HMAC is invalid'
                +          end
                +        end
                @@ -151,3 +199,40 @@
                -      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
                @@ -155 +240,105 @@
                -        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
                @@ -158,2 +347,2 @@
                -      def verify_authenticity!(data, signature)
                -        raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
                +      def initialize(secret, opts = {})
                +        opts = opts.dup
                @@ -161,2 +350,9 @@
                -        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)
                @@ -166,5 +362,10 @@
                -      # 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
                @@ -172 +373,2 @@
                -        return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
                +        decryptor.decrypt(base64_data)
                +      end
                @@ -174,2 +376,8 @@
                -        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
                @@ -177 +385 @@
                -        "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
                +        encryptor.encrypt(message)
                @@ -180,5 +388,3 @@
                -      # 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
                @@ -186,3 +392,20 @@
                -        # 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'
        lib/rack/session/version.rb
                --- /tmp/d20260408-951-t1jzsn/rack-session-2.1.1/lib/rack/session/version.rb	2026-04-08 03:07:04.395270709 +0000
                +++ /tmp/d20260408-951-t1jzsn/rack-session-2.1.2/lib/rack/session/version.rb	2026-04-08 03:07:04.396270707 +0000
                @@ -8 +8 @@
                -    VERSION = "2.1.1"
                +    VERSION = "2.1.2"
        releases.md
                --- /tmp/d20260408-951-t1jzsn/rack-session-2.1.1/releases.md	2026-04-08 03:07:04.395270709 +0000
                +++ /tmp/d20260408-951-t1jzsn/rack-session-2.1.2/releases.md	2026-04-08 03:07:04.397270706 +0000
                @@ -2,0 +3,4 @@
                +## 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.
                +

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT date:
    2.1.1: 2025-05-06 00:00:00 UTC
    2.1.2: 1980-01-02 00:00:00 UTC
  DIFFERENT rubygems_version:
    2.1.1: 3.5.22
    2.1.2: 3.6.9
  DIFFERENT version:
    2.1.1: 2.1.1
    2.1.2: 2.1.2
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
            lib/rack/session.rb +0/-1
            lib/rack/session/cookie.rb +4/-2
            lib/rack/session/encryptor.rb +343/-120
            lib/rack/session/version.rb +1/-1
            releases.md +4/-0

1 similar comment
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT date:
    2.1.1: 2025-05-06 00:00:00 UTC
    2.1.2: 1980-01-02 00:00:00 UTC
  DIFFERENT rubygems_version:
    2.1.1: 3.5.22
    2.1.2: 3.6.9
  DIFFERENT version:
    2.1.1: 2.1.1
    2.1.2: 2.1.2
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
            lib/rack/session.rb +0/-1
            lib/rack/session/cookie.rb +4/-2
            lib/rack/session/encryptor.rb +343/-120
            lib/rack/session/version.rb +1/-1
            releases.md +4/-0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
        lib/rack/session.rb
                --- /tmp/d20260408-971-j04so8/rack-session-2.1.1/lib/rack/session.rb	2026-04-08 03:07:25.312345345 +0000
                +++ /tmp/d20260408-971-j04so8/rack-session-2.1.2/lib/rack/session.rb	2026-04-08 03:07:25.314345332 +0000
                @@ -11 +10,0 @@
                -    autoload :Memcache, "rack/session/memcache"
        lib/rack/session/cookie.rb
                --- /tmp/d20260408-971-j04so8/rack-session-2.1.1/lib/rack/session/cookie.rb	2026-04-08 03:07:25.312345345 +0000
                +++ /tmp/d20260408-971-j04so8/rack-session-2.1.2/lib/rack/session/cookie.rb	2026-04-08 03:07:25.314345332 +0000
                @@ -240,2 +240,4 @@
                -            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.
        lib/rack/session/encryptor.rb
                --- /tmp/d20260408-971-j04so8/rack-session-2.1.1/lib/rack/session/encryptor.rb	2026-04-08 03:07:25.313345338 +0000
                +++ /tmp/d20260408-971-j04so8/rack-session-2.1.2/lib/rack/session/encryptor.rb	2026-04-08 03:07:25.314345332 +0000
                @@ -7,0 +8 @@
                +require 'json'
                @@ -10 +10,0 @@
                -require 'zlib'
                @@ -26,30 +26,35 @@
                -      # 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
                @@ -57,2 +62,33 @@
                -        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
                @@ -60,4 +96,15 @@
                -        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
                @@ -66,3 +113,2 @@
                -        @options = {
                -          serialize_json: false, pad_size: 32, purpose: nil
                -        }.update(opts)
                +        def decrypt(base64_data)
                +          data = Base64.urlsafe_decode64(base64_data)
                @@ -70,2 +116,2 @@
                -        @hmac_secret = secret.dup.force_encoding('BINARY')
                -        @cipher_secret = @hmac_secret.slice!(0, 32)
                +          signature = data.slice!(-32..-1)
                +          verify_authenticity!(data, signature)
                @@ -73,3 +119,2 @@
                -        @hmac_secret.freeze
                -        @cipher_secret.freeze
                -      end
                +          version = data.slice!(0, 1)
                +          raise InvalidMessage, 'wrong version' unless version == "\1"
                @@ -77,2 +122,2 @@
                -      def decrypt(base64_data)
                -        data = Base64.urlsafe_decode64(base64_data)
                +          message_secret = data.slice!(0, 32)
                +          cipher_iv = data.slice!(0, 16)
                @@ -80 +125,2 @@
                -        signature = data.slice!(-32..-1)
                +          cipher = new_cipher
                +          cipher.decrypt
                @@ -82 +128 @@
                -        verify_authenticity! data, signature
                +          set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                @@ -84,4 +130,2 @@
                -        # 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
                @@ -89,2 +133,4 @@
                -        cipher = new_cipher
                -        cipher.decrypt
                +          deserialized_message data
                +        rescue ArgumentError
                +          raise InvalidSignature, 'Message invalid'
                +        end
                @@ -92 +138,2 @@
                -        set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                +        def encrypt(message)
                +          version = "\1"
                @@ -94,2 +141,2 @@
                -        cipher.iv = cipher_iv
                -        data = cipher.update(data) << cipher.final
                +          serialized_payload = serialize_payload(message)
                +          message_secret, cipher_secret = new_message_and_cipher_secret
                @@ -97,4 +144,2 @@
                -        deserialized_message data
                -      rescue ArgumentError
                -        raise InvalidSignature, 'Message invalid'
                -      end
                +          cipher = new_cipher
                +          cipher.encrypt
                @@ -102,2 +147 @@
                -      def encrypt(message)
                -        version = "\1"
                +          set_cipher_key(cipher, cipher_secret)
                @@ -105,2 +149 @@
                -        serialized_payload = serialize_payload(message)
                -        message_secret, cipher_secret = new_message_and_cipher_secret
                +          cipher_iv = cipher.random_iv
                @@ -108,2 +151 @@
                -        cipher = new_cipher
                -        cipher.encrypt
                +          encrypted_data = cipher.update(serialized_payload) << cipher.final
                @@ -111 +153,6 @@
                -        set_cipher_key(cipher, cipher_secret)
                +          data = String.new
                +          data << version
                +          data << message_secret
                +          data << cipher_iv
                +          data << encrypted_data
                +          data << compute_signature(data)
                @@ -113 +160,2 @@
                -        cipher_iv = cipher.random_iv
                +          Base64.urlsafe_encode64(data)
                +        end
                @@ -115 +163 @@
                -        encrypted_data = cipher.update(serialized_payload) << cipher.final
                +        private
                @@ -117,6 +165,3 @@
                -        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
                @@ -124,2 +169,2 @@
                -        Base64.urlsafe_encode64(data)
                -      end
                +        def new_message_and_cipher_secret
                +          message_secret = SecureRandom.random_bytes(32)
                @@ -127 +172,2 @@
                -      private
                +          [message_secret, cipher_secret_from_message_secret(message_secret)]
                +        end
                @@ -129,3 +175,3 @@
                -      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
                @@ -133,2 +179,3 @@
                -      def new_message_and_cipher_secret
                -        message_secret = SecureRandom.random_bytes(32)
                +        def set_cipher_key(cipher, key)
                +          cipher.key = key
                +        end
                @@ -136,2 +183,3 @@
                -        [message_secret, cipher_secret_from_message_secret(message_secret)]
                -      end
                +        def compute_signature(data)
                +          signing_data = data
                +          signing_data += @options[:purpose] if @options[:purpose]
                @@ -139,3 +187,2 @@
                -      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
                @@ -143,3 +190,2 @@
                -      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?
                @@ -147,2 +193,4 @@
                -      def serializer
                -        @serializer ||= @options[:serialize_json] ? JSON : Marshal
                +          unless Rack::Utils.secure_compare(signature, compute_signature(data))
                +            raise InvalidSignature, 'HMAC is invalid'
                +          end
                +        end
                @@ -151,3 +199,40 @@
                -      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
                @@ -155 +240,105 @@
                -        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
                @@ -158,2 +347,2 @@
                -      def verify_authenticity!(data, signature)
                -        raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
                +      def initialize(secret, opts = {})
                +        opts = opts.dup
                @@ -161,2 +350,9 @@
                -        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)
                @@ -166,5 +362,10 @@
                -      # 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
                @@ -172 +373,2 @@
                -        return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
                +        decryptor.decrypt(base64_data)
                +      end
                @@ -174,2 +376,8 @@
                -        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
                @@ -177 +385 @@
                -        "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
                +        encryptor.encrypt(message)
                @@ -180,5 +388,3 @@
                -      # 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
                @@ -186,3 +392,20 @@
                -        # 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'
        lib/rack/session/version.rb
                --- /tmp/d20260408-971-j04so8/rack-session-2.1.1/lib/rack/session/version.rb	2026-04-08 03:07:25.313345338 +0000
                +++ /tmp/d20260408-971-j04so8/rack-session-2.1.2/lib/rack/session/version.rb	2026-04-08 03:07:25.314345332 +0000
                @@ -8 +8 @@
                -    VERSION = "2.1.1"
                +    VERSION = "2.1.2"
        releases.md
                --- /tmp/d20260408-971-j04so8/rack-session-2.1.1/releases.md	2026-04-08 03:07:25.313345338 +0000
                +++ /tmp/d20260408-971-j04so8/rack-session-2.1.2/releases.md	2026-04-08 03:07:25.315345325 +0000
                @@ -2,0 +3,4 @@
                +## 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.
                +

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
        lib/rack/session.rb
                --- /tmp/d20260408-964-1ph94n/rack-session-2.1.1/lib/rack/session.rb	2026-04-08 03:07:30.088824991 +0000
                +++ /tmp/d20260408-964-1ph94n/rack-session-2.1.2/lib/rack/session.rb	2026-04-08 03:07:30.089825002 +0000
                @@ -11 +10,0 @@
                -    autoload :Memcache, "rack/session/memcache"
        lib/rack/session/cookie.rb
                --- /tmp/d20260408-964-1ph94n/rack-session-2.1.1/lib/rack/session/cookie.rb	2026-04-08 03:07:30.088824991 +0000
                +++ /tmp/d20260408-964-1ph94n/rack-session-2.1.2/lib/rack/session/cookie.rb	2026-04-08 03:07:30.090825012 +0000
                @@ -240,2 +240,4 @@
                -            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.
        lib/rack/session/encryptor.rb
                --- /tmp/d20260408-964-1ph94n/rack-session-2.1.1/lib/rack/session/encryptor.rb	2026-04-08 03:07:30.088824991 +0000
                +++ /tmp/d20260408-964-1ph94n/rack-session-2.1.2/lib/rack/session/encryptor.rb	2026-04-08 03:07:30.090825012 +0000
                @@ -7,0 +8 @@
                +require 'json'
                @@ -10 +10,0 @@
                -require 'zlib'
                @@ -26,30 +26,35 @@
                -      # 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
                @@ -57,2 +62,33 @@
                -        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
                @@ -60,4 +96,15 @@
                -        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
                @@ -66,3 +113,2 @@
                -        @options = {
                -          serialize_json: false, pad_size: 32, purpose: nil
                -        }.update(opts)
                +        def decrypt(base64_data)
                +          data = Base64.urlsafe_decode64(base64_data)
                @@ -70,2 +116,2 @@
                -        @hmac_secret = secret.dup.force_encoding('BINARY')
                -        @cipher_secret = @hmac_secret.slice!(0, 32)
                +          signature = data.slice!(-32..-1)
                +          verify_authenticity!(data, signature)
                @@ -73,3 +119,2 @@
                -        @hmac_secret.freeze
                -        @cipher_secret.freeze
                -      end
                +          version = data.slice!(0, 1)
                +          raise InvalidMessage, 'wrong version' unless version == "\1"
                @@ -77,2 +122,2 @@
                -      def decrypt(base64_data)
                -        data = Base64.urlsafe_decode64(base64_data)
                +          message_secret = data.slice!(0, 32)
                +          cipher_iv = data.slice!(0, 16)
                @@ -80 +125,2 @@
                -        signature = data.slice!(-32..-1)
                +          cipher = new_cipher
                +          cipher.decrypt
                @@ -82 +128 @@
                -        verify_authenticity! data, signature
                +          set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                @@ -84,4 +130,2 @@
                -        # 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
                @@ -89,2 +133,4 @@
                -        cipher = new_cipher
                -        cipher.decrypt
                +          deserialized_message data
                +        rescue ArgumentError
                +          raise InvalidSignature, 'Message invalid'
                +        end
                @@ -92 +138,2 @@
                -        set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                +        def encrypt(message)
                +          version = "\1"
                @@ -94,2 +141,2 @@
                -        cipher.iv = cipher_iv
                -        data = cipher.update(data) << cipher.final
                +          serialized_payload = serialize_payload(message)
                +          message_secret, cipher_secret = new_message_and_cipher_secret
                @@ -97,4 +144,2 @@
                -        deserialized_message data
                -      rescue ArgumentError
                -        raise InvalidSignature, 'Message invalid'
                -      end
                +          cipher = new_cipher
                +          cipher.encrypt
                @@ -102,2 +147 @@
                -      def encrypt(message)
                -        version = "\1"
                +          set_cipher_key(cipher, cipher_secret)
                @@ -105,2 +149 @@
                -        serialized_payload = serialize_payload(message)
                -        message_secret, cipher_secret = new_message_and_cipher_secret
                +          cipher_iv = cipher.random_iv
                @@ -108,2 +151 @@
                -        cipher = new_cipher
                -        cipher.encrypt
                +          encrypted_data = cipher.update(serialized_payload) << cipher.final
                @@ -111 +153,6 @@
                -        set_cipher_key(cipher, cipher_secret)
                +          data = String.new
                +          data << version
                +          data << message_secret
                +          data << cipher_iv
                +          data << encrypted_data
                +          data << compute_signature(data)
                @@ -113 +160,2 @@
                -        cipher_iv = cipher.random_iv
                +          Base64.urlsafe_encode64(data)
                +        end
                @@ -115 +163 @@
                -        encrypted_data = cipher.update(serialized_payload) << cipher.final
                +        private
                @@ -117,6 +165,3 @@
                -        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
                @@ -124,2 +169,2 @@
                -        Base64.urlsafe_encode64(data)
                -      end
                +        def new_message_and_cipher_secret
                +          message_secret = SecureRandom.random_bytes(32)
                @@ -127 +172,2 @@
                -      private
                +          [message_secret, cipher_secret_from_message_secret(message_secret)]
                +        end
                @@ -129,3 +175,3 @@
                -      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
                @@ -133,2 +179,3 @@
                -      def new_message_and_cipher_secret
                -        message_secret = SecureRandom.random_bytes(32)
                +        def set_cipher_key(cipher, key)
                +          cipher.key = key
                +        end
                @@ -136,2 +183,3 @@
                -        [message_secret, cipher_secret_from_message_secret(message_secret)]
                -      end
                +        def compute_signature(data)
                +          signing_data = data
                +          signing_data += @options[:purpose] if @options[:purpose]
                @@ -139,3 +187,2 @@
                -      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
                @@ -143,3 +190,2 @@
                -      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?
                @@ -147,2 +193,4 @@
                -      def serializer
                -        @serializer ||= @options[:serialize_json] ? JSON : Marshal
                +          unless Rack::Utils.secure_compare(signature, compute_signature(data))
                +            raise InvalidSignature, 'HMAC is invalid'
                +          end
                +        end
                @@ -151,3 +199,40 @@
                -      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
                @@ -155 +240,105 @@
                -        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
                @@ -158,2 +347,2 @@
                -      def verify_authenticity!(data, signature)
                -        raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
                +      def initialize(secret, opts = {})
                +        opts = opts.dup
                @@ -161,2 +350,9 @@
                -        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)
                @@ -166,5 +362,10 @@
                -      # 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
                @@ -172 +373,2 @@
                -        return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
                +        decryptor.decrypt(base64_data)
                +      end
                @@ -174,2 +376,8 @@
                -        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
                @@ -177 +385 @@
                -        "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
                +        encryptor.encrypt(message)
                @@ -180,5 +388,3 @@
                -      # 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
                @@ -186,3 +392,20 @@
                -        # 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'
        lib/rack/session/version.rb
                --- /tmp/d20260408-964-1ph94n/rack-session-2.1.1/lib/rack/session/version.rb	2026-04-08 03:07:30.089825002 +0000
                +++ /tmp/d20260408-964-1ph94n/rack-session-2.1.2/lib/rack/session/version.rb	2026-04-08 03:07:30.090825012 +0000
                @@ -8 +8 @@
                -    VERSION = "2.1.1"
                +    VERSION = "2.1.2"
        releases.md
                --- /tmp/d20260408-964-1ph94n/rack-session-2.1.1/releases.md	2026-04-08 03:07:30.089825002 +0000
                +++ /tmp/d20260408-964-1ph94n/rack-session-2.1.2/releases.md	2026-04-08 03:07:30.091825023 +0000
                @@ -2,0 +3,4 @@
                +## 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.
                +

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT rubygems_version:
    3.2.3: 3.6.9
    3.2.6: 4.0.6
  DIFFERENT version:
    3.2.3: 3.2.3
    3.2.6: 3.2.6
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0
            lib/rack/directory.rb +6/-3
            lib/rack/files.rb +1/-1
            lib/rack/mock_response.rb +11/-2
            lib/rack/multipart/parser.rb +44/-3
            lib/rack/request.rb +2/-2
            lib/rack/sendfile.rb +2/-2
            lib/rack/static.rb +7/-3
            lib/rack/utils.rb +107/-15
            lib/rack/version.rb +1/-1
  DIFFERENT extra_rdoc_files:
    3.2.3->3.2.6:
      * Changed:
            CHANGELOG.md +61/-0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack 3.2.3 3.2.6

Compared versions: ["3.2.3", "3.2.6"]
  DIFFERENT files:
    3.2.3->3.2.6:
      * Changed:
        CHANGELOG.md
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/CHANGELOG.md	2026-04-08 03:07:58.603808630 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/CHANGELOG.md	2026-04-08 03:07:58.617808696 +0000
                @@ -4,0 +5,35 @@
                +## [3.2.6] - 2026-04-01
                +
                +### Security
                +
                +- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
                +- [CVE-2026-34230](https://github.com/advisories/GHSA-v569-hp3g-36wr) Avoid O(n^2) algorithm in `Rack::Utils.select_best_encoding` which could lead to denial of service.
                +- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
                +- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
                +- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
                +- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
                +- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
                +- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
                +- [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) `Rack::Sendfile` header-based `X-Accel-Mapping` regex injection enables unauthorized `X-Accel-Redirect`.
                +- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
                +- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
                +- [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) Quadratic-time multipart header parsing allows denial of service via escape-heavy quoted parameters.
                +- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
                +
                +## [3.2.5] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +### Fixed
                +
                +- Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
                +
                +## [3.2.4] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -76,0 +112,13 @@
                +## [3.1.20] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [3.1.19] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
                +
                @@ -459,0 +508,13 @@
                +
                +## [2.2.22] - 2026-02-16
                +
                +### Security
                +
                +- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
                +- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
                +
                +## [2.2.21] - 2025-11-03
                +
                +### Fixed
                +
                +- Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
        lib/rack/directory.rb
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/lib/rack/directory.rb	2026-04-08 03:07:58.610808663 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/lib/rack/directory.rb	2026-04-08 03:07:58.625808733 +0000
                @@ -20 +20 @@
                -    DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                +    DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
                @@ -54 +54 @@
                -        show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
                +        show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
                @@ -84,0 +85 @@
                +      @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
                @@ -121 +122,3 @@
                -      return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
                +
                +      expanded_path = ::File.expand_path(::File.join(@root, path_info))
                +      return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
        lib/rack/files.rb
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/lib/rack/files.rb	2026-04-08 03:07:58.610808663 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/lib/rack/files.rb	2026-04-08 03:07:58.626808737 +0000
                @@ -197 +197 @@
                -          CONTENT_LENGTH => body.size.to_s,
                +          CONTENT_LENGTH => body.bytesize.to_s,
        lib/rack/mock_response.rb
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/lib/rack/mock_response.rb	2026-04-08 03:07:58.612808672 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/lib/rack/mock_response.rb	2026-04-08 03:07:58.630808756 +0000
                @@ -2,0 +3 @@
                +require 'stringio'
                @@ -85,2 +86,10 @@
                -      @body.each do |chunk|
                -        buffer << chunk
                +      begin
                +        if @body.respond_to?(:each)
                +          @body.each do |chunk|
                +            buffer << chunk
                +          end
                +        else
                +          @body.call(StringIO.new(buffer))
                +        end
                +      ensure
                +        @body.close if @body.respond_to?(:close)
        lib/rack/multipart/parser.rb
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/lib/rack/multipart/parser.rb	2026-04-08 03:07:58.613808677 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/lib/rack/multipart/parser.rb	2026-04-08 03:07:58.631808761 +0000
                @@ -36 +36 @@
                -    MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
                +    MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
                @@ -82,0 +83,7 @@
                +      bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
                +      PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
                +      private_constant :PARSER_BYTESIZE_LIMIT
                +
                +      CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
                +      private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +
                @@ -119 +126,9 @@
                -        data[1]
                +
                +        unless data[1].empty?
                +          raise Error, "whitespace between boundary parameter name and equal sign"
                +        end
                +        if data.post_match.match?(/boundary\s*=/i)
                +          raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
                +        end
                +
                +        data[2]
                @@ -127,0 +143,4 @@
                +        if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
                +          raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +        end
                +
                @@ -243,0 +263,2 @@
                +        @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
                +        @content_disposition_quoted_escapes = 0
                @@ -254,0 +276 @@
                +        @total_bytes_read &&= nil if io.is_a?(BoundedIO)
                @@ -292,0 +315,6 @@
                +        if @total_bytes_read
                +          @total_bytes_read += content.bytesize
                +          if @total_bytes_read > PARSER_BYTESIZE_LIMIT
                +            raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
                +          end
                +        end
                @@ -341,0 +370,3 @@
                +      OBS_UNFOLD = /\r\n([ \t])/
                +      private_constant :OBS_UNFOLD
                +
                @@ -345,0 +377,2 @@
                +          content_type.gsub!(OBS_UNFOLD, '\1') if content_type
                +
                @@ -348,0 +382,3 @@
                +            # Implement OBS unfolding (RFC 5322 Section 2.2.3)
                +            disposition.gsub!(OBS_UNFOLD, '\1')
                +
                @@ -385,0 +422,5 @@
                +                  @content_disposition_quoted_escapes += 1
                +                  if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
                +                    raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
                +                  end
                +
                @@ -454 +495 @@
                -          raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
                +          raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
        lib/rack/request.rb
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/lib/rack/request.rb	2026-04-08 03:07:58.614808682 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/lib/rack/request.rb	2026-04-08 03:07:58.632808766 +0000
                @@ -726,2 +726,2 @@
                -          # Match any other printable string (except square brackets) as a hostname
                -          (?<address>[[[:graph:]&&[^\[\]]]]*?)
                +          # Match characters allowed by RFC 3986 Section 3.2.2
                +          (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
        lib/rack/sendfile.rb
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/lib/rack/sendfile.rb	2026-04-08 03:07:58.615808686 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/lib/rack/sendfile.rb	2026-04-08 03:07:58.633808770 +0000
                @@ -54 +54 @@
                -  # that it maps to. The middleware performs a simple substitution on the
                +  # that it maps to. The middleware performs a case-insensitive substitution on the
                @@ -189 +189 @@
                -          new_path = path.sub(/\A#{internal}/i, external)
                +          new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
        lib/rack/static.rb
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/lib/rack/static.rb	2026-04-08 03:07:58.615808686 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/lib/rack/static.rb	2026-04-08 03:07:58.634808775 +0000
                @@ -95,0 +96,3 @@
                +      if @urls.kind_of?(Array)
                +        @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
                +      end
                @@ -118 +121 @@
                -      @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
                +      @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
                @@ -167,0 +171,2 @@
                +      path = ::Rack::Utils.unescape_path(path)
                +
                @@ -175 +179,0 @@
                -          path = ::Rack::Utils.unescape(path)
                @@ -178 +182 @@
                -          /\.(#{rule.join('|')})\z/.match?(path)
                +          /\.#{Regexp.union(rule)}\z/.match?(path)
        lib/rack/utils.rb
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/lib/rack/utils.rb	2026-04-08 03:07:58.616808691 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/lib/rack/utils.rb	2026-04-08 03:07:58.635808780 +0000
                @@ -149,3 +149,2 @@
                -    def forwarded_values(forwarded_header)
                -      return nil unless forwarded_header
                -      forwarded_header = forwarded_header.to_s.gsub("\n", ";")
                +    ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
                +    private_constant :ALLOWED_FORWARED_PARAMS
                @@ -153,5 +152,59 @@
                -      forwarded_header.split(';').each_with_object({}) do |field, values|
                -        field.split(',').each do |pair|
                -          pair = pair.split('=').map(&:strip).join('=')
                -          return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
                -          (values[$1.downcase.to_sym] ||= []) << $2
                +    def forwarded_values(forwarded_header)
                +      return unless forwarded_header
                +      header = forwarded_header.to_s.tr("\n", ";")
                +      header.sub!(/\A[\s;,]+/, '')
                +      num_params = num_escapes = 0
                +      max_params = max_escapes = 1024
                +      params = {}
                +
                +      # Parse parameter list
                +      while i = header.index('=')
                +        # Only parse up to max parameters, to avoid potential denial of service
                +        num_params += 1
                +        return if num_params > max_params
                +
                +        # Found end of parameter name, ensure forward progress in loop
                +        param = header.slice!(0, i+1)
                +
                +        # Remove ending equals and preceding whitespace from parameter name
                +        param.chomp!('=')
                +        param.strip!
                +        param.downcase!
                +        return unless param = ALLOWED_FORWARED_PARAMS[param]
                +
                +        if header[0] == '"'
                +          # Parameter value is quoted, parse it, handling backslash escapes
                +          header.slice!(0, 1)
                +          value = String.new
                +
                +          while i = header.index(/(["\\])/)
                +            c = $1
                +
                +            # Append all content until ending quote or escape
                +            value << header.slice!(0, i)
                +
                +            # Remove either backslash or ending quote,
                +            # ensures forward progress in loop
                +            header.slice!(0, 1)
                +
                +            # stop parsing parameter value if found ending quote
                +            break if c == '"'
                +
                +            # Only allow up to max escapes, to avoid potential denial of service
                +            num_escapes += 1
                +            return if num_escapes > max_escapes
                +            escaped_char = header.slice!(0, 1)
                +            value << escaped_char
                +          end
                +        else
                +          if i = header.index(/[;,]/)
                +            # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
                +            value = header.slice!(0, i)
                +            value.sub!(/[\s;,]+\z/, '')
                +          else
                +            # If no ending semicolon, assume remainder of line is value and stop parsing
                +            header.strip!
                +            value = header
                +            header = ''
                +          end
                +          value.lstrip!
                @@ -158,0 +212,5 @@
                +
                +        (params[param] ||= []) << value
                +
                +        # skip trailing semicolons/commas/whitespace, to proceed to next parameter
                +        header.sub!(/\A[\s;,]+/, '') unless header.empty?
                @@ -159,0 +218,2 @@
                +
                +      params
                @@ -195,0 +256,18 @@
                +    # Given an array of available encoding strings, and an array of
                +    # acceptable encodings for a request, where each element of the
                +    # acceptable encodings array is an array where the first element
                +    # is an encoding name and the second element is the numeric
                +    # priority for the encoding, return the available encoding with
                +    # the highest priority.
                +    #
                +    # The accept_encoding argument is typically generated by calling
                +    # Request#accept_encoding.
                +    #
                +    # Example:
                +    #
                +    #   select_best_encoding(%w(compress gzip identity),
                +    #                        [["compress", 0.5], ["gzip", 1.0]])
                +    #   # => "gzip"
                +    #
                +    # To reduce denial of service potential, only the first 16
                +    # acceptable encodings are considered.
                @@ -198,0 +277,2 @@
                +      # Only process the first 16 encodings
                +      accept_encoding = accept_encoding[0...16]
                @@ -199,0 +280 @@
                +      wildcard_seen = false
                @@ -205,2 +286,5 @@
                -          (available_encodings - accept_encoding.map(&:first)).each do |m2|
                -            expanded_accept_encoding << [m2, q, preference]
                +          unless wildcard_seen
                +            (available_encodings - accept_encoding.map(&:first)).each do |m2|
                +              expanded_accept_encoding << [m2, q, preference]
                +            end
                +            wildcard_seen = true
                @@ -214 +298,7 @@
                -        .sort_by { |_, q, p| [-q, p] }
                +        .sort do |(_, q1, p1), (_, q2, p2)|
                +          if r = (q1 <=> q2).nonzero?
                +            -r
                +          else
                +            (p1 <=> p2).nonzero? || 0
                +          end
                +        end
                @@ -402,2 +492,2 @@
                -    def byte_ranges(env, size)
                -      get_byte_ranges env['HTTP_RANGE'], size
                +    def byte_ranges(env, size, max_ranges: 100)
                +      get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
                @@ -406 +496 @@
                -    def get_byte_ranges(http_range, size)
                +    def get_byte_ranges(http_range, size, max_ranges: 100)
                @@ -410,0 +501,2 @@
                +      byte_range = $1
                +      return nil if byte_range.count(',') >= max_ranges
                @@ -412 +504 @@
                -      $1.split(/,[ \t]*/).each do |range_spec|
                +      byte_range.split(/,[ \t]*/).each do |range_spec|
        lib/rack/version.rb
                --- /tmp/d20260408-670-7h2715/rack-3.2.3/lib/rack/version.rb	2026-04-08 03:07:58.616808691 +0000
                +++ /tmp/d20260408-670-7h2715/rack-3.2.6/lib/rack/version.rb	2026-04-08 03:07:58.635808780 +0000
                @@ -9 +9 @@
                -  VERSION = "3.2.3"
                +  VERSION = "3.2.6"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT date:
    2.1.1: 2025-05-06 00:00:00 UTC
    2.1.2: 1980-01-02 00:00:00 UTC
  DIFFERENT rubygems_version:
    2.1.1: 3.5.22
    2.1.2: 3.6.9
  DIFFERENT version:
    2.1.1: 2.1.1
    2.1.2: 2.1.2
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
            lib/rack/session.rb +0/-1
            lib/rack/session/cookie.rb +4/-2
            lib/rack/session/encryptor.rb +343/-120
            lib/rack/session/version.rb +1/-1
            releases.md +4/-0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

gem compare --diff rack-session 2.1.1 2.1.2

Compared versions: ["2.1.1", "2.1.2"]
  DIFFERENT files:
    2.1.1->2.1.2:
      * Changed:
        lib/rack/session.rb
                --- /tmp/d20260408-988-gw9953/rack-session-2.1.1/lib/rack/session.rb	2026-04-08 03:08:40.464015548 +0000
                +++ /tmp/d20260408-988-gw9953/rack-session-2.1.2/lib/rack/session.rb	2026-04-08 03:08:40.470015575 +0000
                @@ -11 +10,0 @@
                -    autoload :Memcache, "rack/session/memcache"
        lib/rack/session/cookie.rb
                --- /tmp/d20260408-988-gw9953/rack-session-2.1.1/lib/rack/session/cookie.rb	2026-04-08 03:08:40.467015562 +0000
                +++ /tmp/d20260408-988-gw9953/rack-session-2.1.2/lib/rack/session/cookie.rb	2026-04-08 03:08:40.471015579 +0000
                @@ -240,2 +240,4 @@
                -            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.
        lib/rack/session/encryptor.rb
                --- /tmp/d20260408-988-gw9953/rack-session-2.1.1/lib/rack/session/encryptor.rb	2026-04-08 03:08:40.467015562 +0000
                +++ /tmp/d20260408-988-gw9953/rack-session-2.1.2/lib/rack/session/encryptor.rb	2026-04-08 03:08:40.471015579 +0000
                @@ -7,0 +8 @@
                +require 'json'
                @@ -10 +10,0 @@
                -require 'zlib'
                @@ -26,30 +26,35 @@
                -      # 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
                @@ -57,2 +62,33 @@
                -        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
                @@ -60,4 +96,15 @@
                -        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
                @@ -66,3 +113,2 @@
                -        @options = {
                -          serialize_json: false, pad_size: 32, purpose: nil
                -        }.update(opts)
                +        def decrypt(base64_data)
                +          data = Base64.urlsafe_decode64(base64_data)
                @@ -70,2 +116,2 @@
                -        @hmac_secret = secret.dup.force_encoding('BINARY')
                -        @cipher_secret = @hmac_secret.slice!(0, 32)
                +          signature = data.slice!(-32..-1)
                +          verify_authenticity!(data, signature)
                @@ -73,3 +119,2 @@
                -        @hmac_secret.freeze
                -        @cipher_secret.freeze
                -      end
                +          version = data.slice!(0, 1)
                +          raise InvalidMessage, 'wrong version' unless version == "\1"
                @@ -77,2 +122,2 @@
                -      def decrypt(base64_data)
                -        data = Base64.urlsafe_decode64(base64_data)
                +          message_secret = data.slice!(0, 32)
                +          cipher_iv = data.slice!(0, 16)
                @@ -80 +125,2 @@
                -        signature = data.slice!(-32..-1)
                +          cipher = new_cipher
                +          cipher.decrypt
                @@ -82 +128 @@
                -        verify_authenticity! data, signature
                +          set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                @@ -84,4 +130,2 @@
                -        # 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
                @@ -89,2 +133,4 @@
                -        cipher = new_cipher
                -        cipher.decrypt
                +          deserialized_message data
                +        rescue ArgumentError
                +          raise InvalidSignature, 'Message invalid'
                +        end
                @@ -92 +138,2 @@
                -        set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
                +        def encrypt(message)
                +          version = "\1"
                @@ -94,2 +141,2 @@
                -        cipher.iv = cipher_iv
                -        data = cipher.update(data) << cipher.final
                +          serialized_payload = serialize_payload(message)
                +          message_secret, cipher_secret = new_message_and_cipher_secret
                @@ -97,4 +144,2 @@
                -        deserialized_message data
                -      rescue ArgumentError
                -        raise InvalidSignature, 'Message invalid'
                -      end
                +          cipher = new_cipher
                +          cipher.encrypt
                @@ -102,2 +147 @@
                -      def encrypt(message)
                -        version = "\1"
                +          set_cipher_key(cipher, cipher_secret)
                @@ -105,2 +149 @@
                -        serialized_payload = serialize_payload(message)
                -        message_secret, cipher_secret = new_message_and_cipher_secret
                +          cipher_iv = cipher.random_iv
                @@ -108,2 +151 @@
                -        cipher = new_cipher
                -        cipher.encrypt
                +          encrypted_data = cipher.update(serialized_payload) << cipher.final
                @@ -111 +153,6 @@
                -        set_cipher_key(cipher, cipher_secret)
                +          data = String.new
                +          data << version
                +          data << message_secret
                +          data << cipher_iv
                +          data << encrypted_data
                +          data << compute_signature(data)
                @@ -113 +160,2 @@
                -        cipher_iv = cipher.random_iv
                +          Base64.urlsafe_encode64(data)
                +        end
                @@ -115 +163 @@
                -        encrypted_data = cipher.update(serialized_payload) << cipher.final
                +        private
                @@ -117,6 +165,3 @@
                -        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
                @@ -124,2 +169,2 @@
                -        Base64.urlsafe_encode64(data)
                -      end
                +        def new_message_and_cipher_secret
                +          message_secret = SecureRandom.random_bytes(32)
                @@ -127 +172,2 @@
                -      private
                +          [message_secret, cipher_secret_from_message_secret(message_secret)]
                +        end
                @@ -129,3 +175,3 @@
                -      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
                @@ -133,2 +179,3 @@
                -      def new_message_and_cipher_secret
                -        message_secret = SecureRandom.random_bytes(32)
                +        def set_cipher_key(cipher, key)
                +          cipher.key = key
                +        end
                @@ -136,2 +183,3 @@
                -        [message_secret, cipher_secret_from_message_secret(message_secret)]
                -      end
                +        def compute_signature(data)
                +          signing_data = data
                +          signing_data += @options[:purpose] if @options[:purpose]
                @@ -139,3 +187,2 @@
                -      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
                @@ -143,3 +190,2 @@
                -      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?
                @@ -147,2 +193,4 @@
                -      def serializer
                -        @serializer ||= @options[:serialize_json] ? JSON : Marshal
                +          unless Rack::Utils.secure_compare(signature, compute_signature(data))
                +            raise InvalidSignature, 'HMAC is invalid'
                +          end
                +        end
                @@ -151,3 +199,40 @@
                -      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
                @@ -155 +240,105 @@
                -        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
                @@ -158,2 +347,2 @@
                -      def verify_authenticity!(data, signature)
                -        raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
                +      def initialize(secret, opts = {})
                +        opts = opts.dup
                @@ -161,2 +350,9 @@
                -        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)
                @@ -166,5 +362,10 @@
                -      # 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
                @@ -172 +373,2 @@
                -        return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
                +        decryptor.decrypt(base64_data)
                +      end
                @@ -174,2 +376,8 @@
                -        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
                @@ -177 +385 @@
                -        "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
                +        encryptor.encrypt(message)
                @@ -180,5 +388,3 @@
                -      # 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
                @@ -186,3 +392,20 @@
                -        # 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'
        lib/rack/session/version.rb
                --- /tmp/d20260408-988-gw9953/rack-session-2.1.1/lib/rack/session/version.rb	2026-04-08 03:08:40.468015566 +0000
                +++ /tmp/d20260408-988-gw9953/rack-session-2.1.2/lib/rack/session/version.rb	2026-04-08 03:08:40.471015579 +0000
                @@ -8 +8 @@
                -    VERSION = "2.1.1"
                +    VERSION = "2.1.2"
        releases.md
                --- /tmp/d20260408-988-gw9953/rack-session-2.1.1/releases.md	2026-04-08 03:08:40.468015566 +0000
                +++ /tmp/d20260408-988-gw9953/rack-session-2.1.2/releases.md	2026-04-08 03:08:40.472015584 +0000
                @@ -2,0 +3,4 @@
                +## 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.
                +

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies ruby Pull requests that update Ruby code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants