Skip to content

Commit 836fdec

Browse files
Merge pull request rails#47708 from rails/fix-am-secure-password-length-validation
Improve Password Length Validation for BCrypt Compatibility
2 parents c2ec6e7 + a60785c commit 836fdec

File tree

4 files changed

+41
-4
lines changed

4 files changed

+41
-4
lines changed

activemodel/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
* Improve password length validation in ActiveModel::SecurePassword to consider byte size for BCrypt compatibility.
2+
3+
The previous password length validation only considered the character count, which may not
4+
accurately reflect the 72-byte size limit imposed by BCrypt. This change updates the validation
5+
to consider both character count and byte size while keeping the character length validation in place.
6+
7+
```ruby
8+
user = User.new(password: "a" * 73) # 73 characters
9+
user.valid? # => false
10+
user.errors[:password] # => ["is too long (maximum is 72 characters)"]
11+
12+
13+
user = User.new(password: "" * 25) # 25 characters, 75 bytes
14+
user.valid? # => false
15+
user.errors[:password] # => ["is too long (maximum is 72 bytes)"]
16+
```
17+
18+
*ChatGPT*, *Guillermo Iguaran*
19+
120
* `has_secure_password` now generates an `#{attribute}_salt` method that returns the salt
221
used to compute the password digest. The salt will change whenever the password is changed,
322
so it can be used to create single-use password reset tokens with `generates_token_for`:

activemodel/lib/active_model/locale/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ en:
1818
too_long:
1919
one: "is too long (maximum is 1 character)"
2020
other: "is too long (maximum is %{count} characters)"
21+
password_too_long: "is too long"
2122
too_short:
2223
one: "is too short (minimum is 1 character)"
2324
other: "is too short (minimum is %{count} characters)"

activemodel/lib/active_model/secure_password.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,14 @@ def has_secure_password(attribute = :password, validations: true)
132132
end
133133
end
134134

135-
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
135+
# Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes).
136+
validate do |record|
137+
password_value = record.public_send(attribute)
138+
if password_value.present? && password_value.bytesize > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
139+
record.errors.add(attribute, :password_too_long)
140+
end
141+
end
142+
136143
validates_confirmation_of attribute, allow_blank: true
137144
end
138145
end

activemodel/test/cases/secure_password_test.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,22 @@ class SecurePasswordTest < ActiveModel::TestCase
6262
assert_equal ["can’t be blank"], @user.errors[:password]
6363
end
6464

65-
test "create a new user with validation and password length greater than 72" do
65+
test "create a new user with validation and password length greater than 72 characters" do
6666
@user.password = "a" * 73
6767
@user.password_confirmation = "a" * 73
6868
assert_not @user.valid?(:create), "user should be invalid"
6969
assert_equal 1, @user.errors.count
70-
assert_equal ["is too long (maximum is 72 characters)"], @user.errors[:password]
70+
assert_equal ["is too long"], @user.errors[:password]
71+
end
72+
73+
test "create a new user with validation and password byte size greater than 72 bytes" do
74+
# Create a password with 73 bytes by using a 3-byte Unicode character (e.g., "あ") 24 times, followed by a 1-byte character "a".
75+
# This will result in a password length of 25 characters, but with a byte size of 73.
76+
@user.password = "あ" * 24 + "a"
77+
@user.password_confirmation = "あ" * 24 + "a"
78+
assert_not @user.valid?(:create), "user should be invalid"
79+
assert_equal 1, @user.errors.count
80+
assert_equal ["is too long"], @user.errors[:password]
7181
end
7282

7383
test "create a new user with validation and a blank password confirmation" do
@@ -142,7 +152,7 @@ class SecurePasswordTest < ActiveModel::TestCase
142152
@existing_user.password_confirmation = "a" * 73
143153
assert_not @existing_user.valid?(:update), "user should be invalid"
144154
assert_equal 1, @existing_user.errors.count
145-
assert_equal ["is too long (maximum is 72 characters)"], @existing_user.errors[:password]
155+
assert_equal ["is too long"], @existing_user.errors[:password]
146156
end
147157

148158
test "updating an existing user with validation and a blank password confirmation" do

0 commit comments

Comments
 (0)