From ee89ecbc8f2ece7b2441ee0edd8d664421a5688d Mon Sep 17 00:00:00 2001
From: Jeremy Evans <code@jeremyevans.net>
Date: Mon, 23 May 2022 10:51:20 -0700
Subject: [PATCH] Work around implicit null byte check added in bcrypt 3.1.18
 by checking password requirements before other password checks

An undocumented change in bcrypt 3.1.18 causes it to raise
ArgumentError for passwords with null bytes, due to a change to
use StringValueCStr instead of StringValuePtr in the change to
unlock the GVL while calculating the password hash.

This works around the issue by performing Rodauth's checks on the
password before passing the password to bcrypt.
---
 CHANGELOG                              | 2 ++
 lib/rodauth/features/reset_password.rb | 8 ++++----
 spec/reset_password_spec.rb            | 2 +-
 3 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 07658a3b..1ed56ed2 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,7 @@
 === master
 
+* Work around implicit null byte check added in bcrypt 3.1.18 by checking password requirements before other password checks (jeremyevans)
+
 * Fix invalid HTML on pages with OTP QR codes (jeremyevans)
 
 * Add recovery_codes_available? configuration method to the recovery_codes feature (janko) (#238)
diff --git a/lib/rodauth/features/reset_password.rb b/lib/rodauth/features/reset_password.rb
index 3f1ddc92..3b97bf08 100644
--- a/lib/rodauth/features/reset_password.rb
+++ b/lib/rodauth/features/reset_password.rb
@@ -130,6 +130,10 @@ module Rodauth
 
         password = param(password_param)
         catch_error do
+          unless password_meets_requirements?(password)
+            throw_error_status(invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
+          end
+
           if password_match?(password) 
             throw_error_reason(:same_as_existing_password, invalid_field_error_status, password_param, same_as_existing_password_message)
           end
@@ -138,10 +142,6 @@ module Rodauth
             throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
           end
 
-          unless password_meets_requirements?(password)
-            throw_error_status(invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
-          end
-
           transaction do
             before_reset_password
             set_password(password)
diff --git a/spec/reset_password_spec.rb b/spec/reset_password_spec.rb
index b1be24ef..866046c6 100644
--- a/spec/reset_password_spec.rb
+++ b/spec/reset_password_spec.rb
@@ -272,7 +272,7 @@ def rodauth.raised_uniqueness_violation(*) StandardError.new; end
       res = json_request('/reset-password', :key=>link[4...-1])
       res.must_equal [401, {"reason"=>"invalid_reset_password_key", "error"=>"There was an error resetting your password"}]
 
-      res = json_request('/reset-password', :key=>link[4..-1], :password=>'1', "password-confirm"=>'2')
+      res = json_request('/reset-password', :key=>link[4..-1], :password=>'ab1234561', "password-confirm"=>'ab1234562')
       res.must_equal [422, {'reason'=>"passwords_do_not_match","error"=>"There was an error resetting your password", "field-error"=>["password", 'passwords do not match']}]
 
       res = json_request('/reset-password', :key=>link[4..-1], :password=>'0123456789', "password-confirm"=>'0123456789')