VYPR
Medium severityGHSA Advisory· Published May 9, 2026· Updated May 13, 2026

CVE-2026-42256

CVE-2026-42256

Description

Net::IMAP implements Internet Message Access Protocol (IMAP) client functionality in Ruby. From versions 0.4.0 to before 0.4.24, 0.5.0 to before 0.5.14, and 0.6.0 to before 0.6.4, when authenticating a connection with SCRAM-SHA1 or SCRAM-SHA256, a hostile server can perform a computational denial-of-service attack on the client process by sending a big iteration count value. This issue has been patched in versions 0.4.24, 0.5.14, and 0.6.4.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
net-imapRubyGems
>= 0.6.0, < 0.6.40.6.4
net-imapRubyGems
>= 0.5.0, < 0.5.140.5.14
net-imapRubyGems
>= 0.4.0, < 0.4.240.4.24

Affected products

1

Patches

3
808001bc45c0

🔀 Merge pull request #656 from ruby/backport/v0.5/scram-maximum_iterations

https://github.com/ruby/net-imapnicholas a. evansApr 22, 2026via ghsa
2 files changed · +133 0
  • lib/net/imap/sasl/scram_authenticator.rb+74 0 modified
    @@ -75,13 +75,19 @@ class ScramAuthenticator
             # * #password ― Password or passphrase associated with this #username.
             # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
             # * _optional_ #min_iterations - Overrides the default value (4096).
    +        # * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
             #
             # Any other keyword parameters are quietly ignored.
    +        #
    +        # *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
    +        # and maximum iteration counts that are appropriate for their security
    +        # context.
             def initialize(username_arg = nil, password_arg = nil,
                            authcid: nil, username: nil,
                            authzid: nil,
                            password: nil, secret: nil,
                            min_iterations: 4096, # see both RFC5802 and RFC7677
    +                       max_iterations: 2**31 - 1,  # max int32
                            cnonce: nil, # must only be set in tests
                            **options)
               @username = username || username_arg || authcid or
    @@ -94,7 +100,22 @@ def initialize(username_arg = nil, password_arg = nil,
               @min_iterations.positive? or
                 raise ArgumentError, "min_iterations must be positive"
     
    +          @max_iterations = Integer max_iterations.to_int
    +          @min_iterations <= @max_iterations or
    +            raise ArgumentError, "max_iterations must be more than min_iterations"
    +
               @cnonce = cnonce || SecureRandom.base64(32)
    +
    +          # These attrs are set from the server challenges
    +          @server_first_message = @snonce = @salt = @iterations = nil
    +          @server_error = nil
    +
    +          # Memoized after @salt and @iterations have been sent.
    +          @salted_password = @client_key = @server_key = nil
    +
    +          # These values are created and cached in response to server challenges
    +          @client_first_message_bare = nil
    +          @client_final_message_without_proof = nil
             end
     
             # Authentication identity: the identity that matches the #password.
    @@ -127,8 +148,43 @@ def initialize(username_arg = nil, password_arg = nil,
     
             # The minimal allowed iteration count.  Lower #iterations will raise an
             # Error.
    +        #
    +        # *WARNING:* The default value (4096) is set to match guidance from
    +        # both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
    +        # and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
    +        # {modern recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
    +        # are significantly higher.
    +        #
    +        # It is ultimately the server's responsibility to securely store
    +        # password hashes.  While this parameter can alert the user to
    +        # insecure password storage and prevent insecure authentication
    +        # exchange, updating the iteration count generally requires resetting
    +        # the password on the server.
             attr_reader :min_iterations
     
    +        # The maximal allowed iteration count.  Higher #iterations will raise an
    +        # Error.
    +        #
    +        # As noted in {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
    +        # >>>
    +        #   A hostile server can perform a computational denial-of-service
    +        #   attack on clients by sending a big iteration count value.
    +        #
    +        # *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
    +        # 32-bit integer.  This is large enough for the computation to take
    +        # several minutes, and insufficient protection against hostile servers.
    +        #
    +        # Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
    +        # blocking C function, and cannot be interrupted by +Timeout+ or
    +        # <tt>Thread.raise</tt>.  And it keeps the Global VM lock, as of v4.0 of
    +        # the +openssl+ gem, so other ruby threads will not be able to run.
    +        #
    +        # <em>To prevent a denial of service attack,</em> this must be set to a
    +        # safe value, depending on hardware and version of OpenSSL.  <em>It is
    +        # the user's responsibility</em> to enforce minimum and maximum
    +        # iteration counts that are appropriate for their security context.
    +        attr_reader :max_iterations
    +
             # The client nonce, generated by SecureRandom
             attr_reader :cnonce
     
    @@ -147,6 +203,15 @@ def initialize(username_arg = nil, password_arg = nil,
             # Net::IMAP::NoResponseError.
             attr_reader :server_error
     
    +        # Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
    +        def salted_password; @salted_password ||= compute_salted { super } end
    +
    +        # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
    +        def client_key; @client_key ||= compute_salted { super } end
    +
    +        # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
    +        def server_key; @server_key ||= compute_salted { super } end
    +
             # Returns a new OpenSSL::Digest object, set to the appropriate hash
             # function for the chosen mechanism.
             #
    @@ -186,6 +251,13 @@ def done?; @state == :done end
     
             private
     
    +        # Checks for +salt+ and +iterations+ before yielding
    +        def compute_salted
    +          salt       in String  or raise Error, "unknown salt"
    +          iterations in Integer or raise Error, "unknown iterations"
    +          yield
    +        end
    +
             # Need to store this for auth_message
             attr_reader :server_first_message
     
    @@ -202,6 +274,8 @@ def recv_server_first_message(server_first_message)
                 raise Error, "server did not send iteration count"
               min_iterations <= iterations or
                 raise Error, "too few iterations: %d" % [iterations]
    +          max_iterations.nil? || iterations <= max_iterations or
    +            raise Error, "too many iterations: %d" % [iterations]
               mext = sparams["m"] and
                 raise Error, "mandatory extension: %p" % [mext]
               snonce.start_with? cnonce or
    
  • test/net/imap/test_authenticators.rb+59 0 modified
    @@ -213,6 +213,65 @@ def test_scram_sha256_authenticator
         assert authenticator.done?
       end
     
    +  def test_scram_min_iterations
    +    # i=1000, below default of 4096
    +    authenticator = scram_sha1("user", "pencil",
    +                               cnonce: "fyko+d2lbbFgONRv9qkxdawL")
    +    assert_equal         4_096, authenticator.min_iterations
    +    assert_equal 2_147_483_647, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too few iterations/) do
    +      authenticator.process(
    +        "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j," \
    +        "s=QSXCR+Q6sek8bf92," \
    +        "i=1000")
    +    end
    +
    +    # i=4096, below configured 100,000
    +    authenticator = scram_sha256("user", "pencil",
    +                                 cnonce: "rOprNGfwEbeRWgbNEkqO",
    +                                 min_iterations: 100_000)
    +    assert_equal       100_000, authenticator.min_iterations
    +    assert_equal 2_147_483_647, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too few iterations/) do
    +      authenticator.process(
    +        "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," \
    +        "s=W22ZaJ0SNY7soEsUEjb6gQ==," \
    +        "i=4096")
    +    end
    +  end
    +
    +  def test_scram_max_iterations
    +    # i=1000, below default of 4096
    +    authenticator = scram_sha1("user", "pencil",
    +                               cnonce: "fyko+d2lbbFgONRv9qkxdawL",
    +                               max_iterations: 1_000_000)
    +    assert_equal     4_096, authenticator.min_iterations
    +    assert_equal 1_000_000, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too many iterations/) do
    +      authenticator.process(
    +        "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j," \
    +        "s=QSXCR+Q6sek8bf92," \
    +        "i=2147483647")
    +    end
    +
    +    # i=4096, below configured 100,000
    +    authenticator = scram_sha256("user", "pencil",
    +                                 cnonce: "rOprNGfwEbeRWgbNEkqO",
    +                                 max_iterations: 100_000)
    +    assert_equal   4_096, authenticator.min_iterations
    +    assert_equal 100_000, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too many iterations/) do
    +      authenticator.process(
    +        "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," \
    +        "s=W22ZaJ0SNY7soEsUEjb6gQ==," \
    +        "i=2147483647")
    +    end
    +  end
    +
       # ----------------------
       # XOAUTH2
       # ----------------------
    
158d0b505074

🔀 Merge pull request #655 from ruby/backport/v0.4/scram-maximum_iterations

https://github.com/ruby/net-imapnicholas a. evansApr 22, 2026via ghsa
2 files changed · +133 0
  • lib/net/imap/sasl/scram_authenticator.rb+74 0 modified
    @@ -75,13 +75,19 @@ class ScramAuthenticator
             # * #password ― Password or passphrase associated with this #username.
             # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
             # * _optional_ #min_iterations - Overrides the default value (4096).
    +        # * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
             #
             # Any other keyword parameters are quietly ignored.
    +        #
    +        # *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
    +        # and maximum iteration counts that are appropriate for their security
    +        # context.
             def initialize(username_arg = nil, password_arg = nil,
                            authcid: nil, username: nil,
                            authzid: nil,
                            password: nil, secret: nil,
                            min_iterations: 4096, # see both RFC5802 and RFC7677
    +                       max_iterations: 2**31 - 1,  # max int32
                            cnonce: nil, # must only be set in tests
                            **options)
               @username = username || username_arg || authcid or
    @@ -94,7 +100,22 @@ def initialize(username_arg = nil, password_arg = nil,
               @min_iterations.positive? or
                 raise ArgumentError, "min_iterations must be positive"
     
    +          @max_iterations = Integer max_iterations.to_int
    +          @min_iterations <= @max_iterations or
    +            raise ArgumentError, "max_iterations must be more than min_iterations"
    +
               @cnonce = cnonce || SecureRandom.base64(32)
    +
    +          # These attrs are set from the server challenges
    +          @server_first_message = @snonce = @salt = @iterations = nil
    +          @server_error = nil
    +
    +          # Memoized after @salt and @iterations have been sent.
    +          @salted_password = @client_key = @server_key = nil
    +
    +          # These values are created and cached in response to server challenges
    +          @client_first_message_bare = nil
    +          @client_final_message_without_proof = nil
             end
     
             # Authentication identity: the identity that matches the #password.
    @@ -127,8 +148,43 @@ def initialize(username_arg = nil, password_arg = nil,
     
             # The minimal allowed iteration count.  Lower #iterations will raise an
             # Error.
    +        #
    +        # *WARNING:* The default value (4096) is set to match guidance from
    +        # both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
    +        # and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
    +        # {modern recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
    +        # are significantly higher.
    +        #
    +        # It is ultimately the server's responsibility to securely store
    +        # password hashes.  While this parameter can alert the user to
    +        # insecure password storage and prevent insecure authentication
    +        # exchange, updating the iteration count generally requires resetting
    +        # the password on the server.
             attr_reader :min_iterations
     
    +        # The maximal allowed iteration count.  Higher #iterations will raise an
    +        # Error.
    +        #
    +        # As noted in {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
    +        # >>>
    +        #   A hostile server can perform a computational denial-of-service
    +        #   attack on clients by sending a big iteration count value.
    +        #
    +        # *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
    +        # 32-bit integer.  This is large enough for the computation to take
    +        # several minutes, and insufficient protection against hostile servers.
    +        #
    +        # Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
    +        # blocking C function, and cannot be interrupted by +Timeout+ or
    +        # <tt>Thread.raise</tt>.  And it keeps the Global VM lock, as of v4.0 of
    +        # the +openssl+ gem, so other ruby threads will not be able to run.
    +        #
    +        # <em>To prevent a denial of service attack,</em> this must be set to a
    +        # safe value, depending on hardware and version of OpenSSL.  <em>It is
    +        # the user's responsibility</em> to enforce minimum and maximum
    +        # iteration counts that are appropriate for their security context.
    +        attr_reader :max_iterations
    +
             # The client nonce, generated by SecureRandom
             attr_reader :cnonce
     
    @@ -147,6 +203,15 @@ def initialize(username_arg = nil, password_arg = nil,
             # Net::IMAP::NoResponseError.
             attr_reader :server_error
     
    +        # Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
    +        def salted_password; @salted_password ||= compute_salted { super } end
    +
    +        # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
    +        def client_key; @client_key ||= compute_salted { super } end
    +
    +        # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
    +        def server_key; @server_key ||= compute_salted { super } end
    +
             # Returns a new OpenSSL::Digest object, set to the appropriate hash
             # function for the chosen mechanism.
             #
    @@ -186,6 +251,13 @@ def done?; @state == :done end
     
             private
     
    +        # Checks for +salt+ and +iterations+ before yielding
    +        def compute_salted
    +          String === salt        or raise Error, "unknown salt"
    +          Integer === iterations or raise Error, "unknown iterations"
    +          yield
    +        end
    +
             # Need to store this for auth_message
             attr_reader :server_first_message
     
    @@ -202,6 +274,8 @@ def recv_server_first_message(server_first_message)
                 raise Error, "server did not send iteration count"
               min_iterations <= iterations or
                 raise Error, "too few iterations: %d" % [iterations]
    +          max_iterations.nil? || iterations <= max_iterations or
    +            raise Error, "too many iterations: %d" % [iterations]
               mext = sparams["m"] and
                 raise Error, "mandatory extension: %p" % [mext]
               snonce.start_with? cnonce or
    
  • test/net/imap/test_imap_authenticators.rb+59 0 modified
    @@ -206,6 +206,65 @@ def test_scram_sha256_authenticator
         assert authenticator.done?
       end
     
    +  def test_scram_min_iterations
    +    # i=1000, below default of 4096
    +    authenticator = scram_sha1("user", "pencil",
    +                               cnonce: "fyko+d2lbbFgONRv9qkxdawL")
    +    assert_equal         4_096, authenticator.min_iterations
    +    assert_equal 2_147_483_647, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too few iterations/) do
    +      authenticator.process(
    +        "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j," \
    +        "s=QSXCR+Q6sek8bf92," \
    +        "i=1000")
    +    end
    +
    +    # i=4096, below configured 100,000
    +    authenticator = scram_sha256("user", "pencil",
    +                                 cnonce: "rOprNGfwEbeRWgbNEkqO",
    +                                 min_iterations: 100_000)
    +    assert_equal       100_000, authenticator.min_iterations
    +    assert_equal 2_147_483_647, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too few iterations/) do
    +      authenticator.process(
    +        "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," \
    +        "s=W22ZaJ0SNY7soEsUEjb6gQ==," \
    +        "i=4096")
    +    end
    +  end
    +
    +  def test_scram_max_iterations
    +    # i=1000, below default of 4096
    +    authenticator = scram_sha1("user", "pencil",
    +                               cnonce: "fyko+d2lbbFgONRv9qkxdawL",
    +                               max_iterations: 1_000_000)
    +    assert_equal     4_096, authenticator.min_iterations
    +    assert_equal 1_000_000, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too many iterations/) do
    +      authenticator.process(
    +        "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j," \
    +        "s=QSXCR+Q6sek8bf92," \
    +        "i=2147483647")
    +    end
    +
    +    # i=4096, below configured 100,000
    +    authenticator = scram_sha256("user", "pencil",
    +                                 cnonce: "rOprNGfwEbeRWgbNEkqO",
    +                                 max_iterations: 100_000)
    +    assert_equal   4_096, authenticator.min_iterations
    +    assert_equal 100_000, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too many iterations/) do
    +      authenticator.process(
    +        "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," \
    +        "s=W22ZaJ0SNY7soEsUEjb6gQ==," \
    +        "i=2147483647")
    +    end
    +  end
    +
       # ----------------------
       # XOAUTH2
       # ----------------------
    
99f59eab6064

🔀 Merge pull request #654 from ruby/scram-maximum_iterations

https://github.com/ruby/net-imapnicholas a. evansApr 22, 2026via ghsa
3 files changed · +173 0
  • benchmarks/openssl-pbkdf2.yml+40 0 added
    @@ -0,0 +1,40 @@
    +prelude: |
    +  require "openssl"
    +  require "securerandom"
    +
    +  PWDS = Array.new(1024) {
    +    Array.new(rand(1..8)) {
    +      Array.new(4) { rand(97..122).chr }.join + rand(0..9).to_s
    +    }.join(" -_".chars.sample)
    +  }
    +  SALTS = Array.new(1024) { SecureRandom.random_bytes(16) }
    +  @idx = 0
    +
    +  $sha1_len   = OpenSSL::Digest.new("SHA1")  .digest_length
    +  $sha256_len = OpenSSL::Digest.new("SHA256").digest_length
    +
    +  def sha1(iterations)
    +    pass, salt, @idx = PWDS[@idx], SALTS[@idx], (@idx + 1) & 1023
    +    OpenSSL::KDF.pbkdf2_hmac(
    +      pass, salt:, iterations:, hash: "SHA1", length: $sha1_len
    +    )
    +  end
    +
    +  def sha256(iterations)
    +    pass, salt, @idx = PWDS[@idx], SALTS[@idx], (@idx + 1) & 1023
    +    OpenSSL::KDF.pbkdf2_hmac(
    +      pass, salt:, iterations:, hash: "SHA256", length: $sha256_len
    +    )
    +  end
    +
    +benchmark:
    +
    +  # SHA1
    +  "SHA1,     1m iterations": "sha1   1_000_000"
    +  "SHA1,    10m iterations": "sha1  10_000_000"
    +  "SHA1,   100m iterations": "sha1 100_000_000"
    +
    +  # SHA256
    +  "SHA256,   1m iterations": "sha256   1_000_000"
    +  "SHA256,  10m iterations": "sha256  10_000_000"
    +  "SHA256, 100m iterations": "sha256 100_000_000"
    
  • lib/net/imap/sasl/scram_authenticator.rb+74 0 modified
    @@ -75,13 +75,19 @@ class ScramAuthenticator
             # * #password ― Password or passphrase associated with this #username.
             # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
             # * _optional_ #min_iterations - Overrides the default value (4096).
    +        # * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
             #
             # Any other keyword parameters are quietly ignored.
    +        #
    +        # *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
    +        # and maximum iteration counts that are appropriate for their security
    +        # context.
             def initialize(username_arg = nil, password_arg = nil,
                            authcid: nil, username: nil,
                            authzid: nil,
                            password: nil, secret: nil,
                            min_iterations: 4096, # see both RFC5802 and RFC7677
    +                       max_iterations: 2**31 - 1,  # max int32
                            cnonce: nil, # must only be set in tests
                            **options)
               @username = username || username_arg || authcid or
    @@ -94,7 +100,22 @@ def initialize(username_arg = nil, password_arg = nil,
               @min_iterations.positive? or
                 raise ArgumentError, "min_iterations must be positive"
     
    +          @max_iterations = Integer max_iterations.to_int
    +          @min_iterations <= @max_iterations or
    +            raise ArgumentError, "max_iterations must be more than min_iterations"
    +
               @cnonce = cnonce || SecureRandom.base64(32)
    +
    +          # These attrs are set from the server challenges
    +          @server_first_message = @snonce = @salt = @iterations = nil
    +          @server_error = nil
    +
    +          # Memoized after @salt and @iterations have been sent.
    +          @salted_password = @client_key = @server_key = nil
    +
    +          # These values are created and cached in response to server challenges
    +          @client_first_message_bare = nil
    +          @client_final_message_without_proof = nil
             end
     
             # Authentication identity: the identity that matches the #password.
    @@ -127,8 +148,43 @@ def initialize(username_arg = nil, password_arg = nil,
     
             # The minimal allowed iteration count.  Lower #iterations will raise an
             # Error.
    +        #
    +        # *WARNING:* The default value (4096) is set to match guidance from
    +        # both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
    +        # and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
    +        # {modern recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
    +        # are significantly higher.
    +        #
    +        # It is ultimately the server's responsibility to securely store
    +        # password hashes.  While this parameter can alert the user to
    +        # insecure password storage and prevent insecure authentication
    +        # exchange, updating the iteration count generally requires resetting
    +        # the password on the server.
             attr_reader :min_iterations
     
    +        # The maximal allowed iteration count.  Higher #iterations will raise an
    +        # Error.
    +        #
    +        # As noted in {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
    +        # >>>
    +        #   A hostile server can perform a computational denial-of-service
    +        #   attack on clients by sending a big iteration count value.
    +        #
    +        # *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
    +        # 32-bit integer.  This is large enough for the computation to take
    +        # several minutes, and insufficient protection against hostile servers.
    +        #
    +        # Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
    +        # blocking C function, and cannot be interrupted by +Timeout+ or
    +        # <tt>Thread.raise</tt>.  And it keeps the Global VM lock, as of v4.0 of
    +        # the +openssl+ gem, so other ruby threads will not be able to run.
    +        #
    +        # <em>To prevent a denial of service attack,</em> this must be set to a
    +        # safe value, depending on hardware and version of OpenSSL.  <em>It is
    +        # the user's responsibility</em> to enforce minimum and maximum
    +        # iteration counts that are appropriate for their security context.
    +        attr_reader :max_iterations
    +
             # The client nonce, generated by SecureRandom
             attr_reader :cnonce
     
    @@ -147,6 +203,15 @@ def initialize(username_arg = nil, password_arg = nil,
             # Net::IMAP::NoResponseError.
             attr_reader :server_error
     
    +        # Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
    +        def salted_password = @salted_password ||= compute_salted { super }
    +
    +        # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
    +        def client_key = @client_key ||= compute_salted { super }
    +
    +        # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
    +        def server_key = @server_key ||= compute_salted { super }
    +
             # Returns a new OpenSSL::Digest object, set to the appropriate hash
             # function for the chosen mechanism.
             #
    @@ -186,6 +251,13 @@ def done?; @state == :done end
     
             private
     
    +        # Checks for +salt+ and +iterations+ before yielding
    +        def compute_salted
    +          salt       in String  or raise Error, "unknown salt"
    +          iterations in Integer or raise Error, "unknown iterations"
    +          yield
    +        end
    +
             # Need to store this for auth_message
             attr_reader :server_first_message
     
    @@ -202,6 +274,8 @@ def recv_server_first_message(server_first_message)
                 raise Error, "server did not send iteration count"
               min_iterations <= iterations or
                 raise Error, "too few iterations: %d" % [iterations]
    +          max_iterations.nil? || iterations <= max_iterations or
    +            raise Error, "too many iterations: %d" % [iterations]
               mext = sparams["m"] and
                 raise Error, "mandatory extension: %p" % [mext]
               snonce.start_with? cnonce or
    
  • test/net/imap/test_authenticators.rb+59 0 modified
    @@ -213,6 +213,65 @@ def test_scram_sha256_authenticator
         assert authenticator.done?
       end
     
    +  def test_scram_min_iterations
    +    # i=1000, below default of 4096
    +    authenticator = scram_sha1("user", "pencil",
    +                               cnonce: "fyko+d2lbbFgONRv9qkxdawL")
    +    assert_equal         4_096, authenticator.min_iterations
    +    assert_equal 2_147_483_647, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too few iterations/) do
    +      authenticator.process(
    +        "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j," \
    +        "s=QSXCR+Q6sek8bf92," \
    +        "i=1000")
    +    end
    +
    +    # i=4096, below configured 100,000
    +    authenticator = scram_sha256("user", "pencil",
    +                                 cnonce: "rOprNGfwEbeRWgbNEkqO",
    +                                 min_iterations: 100_000)
    +    assert_equal       100_000, authenticator.min_iterations
    +    assert_equal 2_147_483_647, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too few iterations/) do
    +      authenticator.process(
    +        "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," \
    +        "s=W22ZaJ0SNY7soEsUEjb6gQ==," \
    +        "i=4096")
    +    end
    +  end
    +
    +  def test_scram_max_iterations
    +    # i=1000, below default of 4096
    +    authenticator = scram_sha1("user", "pencil",
    +                               cnonce: "fyko+d2lbbFgONRv9qkxdawL",
    +                               max_iterations: 1_000_000)
    +    assert_equal     4_096, authenticator.min_iterations
    +    assert_equal 1_000_000, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too many iterations/) do
    +      authenticator.process(
    +        "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j," \
    +        "s=QSXCR+Q6sek8bf92," \
    +        "i=2147483647")
    +    end
    +
    +    # i=4096, below configured 100,000
    +    authenticator = scram_sha256("user", "pencil",
    +                                 cnonce: "rOprNGfwEbeRWgbNEkqO",
    +                                 max_iterations: 100_000)
    +    assert_equal   4_096, authenticator.min_iterations
    +    assert_equal 100_000, authenticator.max_iterations
    +    authenticator.process(nil)
    +    assert_raise_with_message(Net::IMAP::SASL::Error, /too many iterations/) do
    +      authenticator.process(
    +        "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," \
    +        "s=W22ZaJ0SNY7soEsUEjb6gQ==," \
    +        "i=2147483647")
    +    end
    +  end
    +
       # ----------------------
       # XOAUTH2
       # ----------------------
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

1