Skip to content

Commit 0fc0693

Browse files
committed
add validation
1 parent 627d63d commit 0fc0693

5 files changed

Lines changed: 33 additions & 2 deletions

File tree

config/locales/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,4 @@ en:
6363
not_saved:
6464
one: "1 error prohibited this %{resource} from being saved:"
6565
other: "%{count} errors prohibited this %{resource} from being saved:"
66+
password_too_long_for_bcrypt: "too long (maximum is 72 bytes)"

devise.gemspec

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,11 @@ Gem::Specification.new do |s|
3232
s.add_dependency("bcrypt", "~> 3.0")
3333
s.add_dependency("railties", ">= 7.0")
3434
s.add_dependency("responders")
35+
36+
s.post_install_message = %q{
37+
[DEVISE] Devise now strictly enforces a 72-byte limit on passwords.
38+
This prevents a known BCrypt security issue where passwords exceeding 72 bytes are silently truncated, potentially causing hash collisions.
39+
40+
This new validation runs automatically alongside your existing character length checks, specifically targeting passwords with heavy multi-byte characters (like emojis) that might look short but are large in memory.
41+
}
3542
end

lib/devise.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ module Test
117117

118118
# Range validation for password length
119119
mattr_accessor :password_length
120-
@@password_length = 6..72 # max 72 byte for bcrypt
120+
@@password_length = 6..72 # max 72 bytes for bcrypt
121121

122122
# The time the user will be remembered without asking for credentials again.
123123
mattr_accessor :remember_for

lib/devise/models/validatable.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module Models
1212
# Validatable adds the following options to +devise+:
1313
#
1414
# * +email_regexp+: the regular expression used to validate e-mails;
15-
# * +password_length+: a range expressing password length. Defaults to 6..128.
15+
# * +password_length+: a range expressing password length. Defaults to 6..72.
1616
#
1717
# Since +password_length+ is applied in a proc within `validates_length_of` it can be overridden
1818
# at runtime.
@@ -21,6 +21,9 @@ module Validatable
2121
VALIDATIONS = [:validates_presence_of, :validates_uniqueness_of, :validates_format_of,
2222
:validates_confirmation_of, :validates_length_of].freeze
2323

24+
# maximum allowed bytes for BCrypt (72 bytes)
25+
MAX_PASSWORD_BCRYPT_LENGTH_ALLOWED = 72
26+
2427
def self.required_fields(klass)
2528
[]
2629
end
@@ -37,6 +40,8 @@ def self.included(base)
3740
validates_presence_of :password, if: :password_required?
3841
validates_confirmation_of :password, if: :password_required?
3942
validates_length_of :password, minimum: proc { password_length.min }, maximum: proc { password_length.max }, allow_blank: true
43+
44+
validate :max_password_length_for_bcrypt
4045
end
4146
end
4247

@@ -62,6 +67,16 @@ def email_required?
6267
true
6368
end
6469

70+
# Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes)
71+
def max_password_length_for_bcrypt
72+
if password.present?
73+
password_already_too_long = self.errors.where(:password, :too_long).present?
74+
if !password_already_too_long && password.bytesize > MAX_PASSWORD_BCRYPT_LENGTH_ALLOWED
75+
self.errors.add(:password, :password_too_long_for_bcrypt)
76+
end
77+
end
78+
end
79+
6580
module ClassMethods
6681
Devise::Models.config(self, :email_regexp, :password_length)
6782
end

test/models/validatable_test.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ class ValidatableTest < ActiveSupport::TestCase
9292
assert_equal 'is too long (maximum is 72 characters)', user.errors[:password].join
9393
end
9494

95+
test 'should validate that password cannot be bigger that 72 bytes for bcrypt' do
96+
Devise.stubs(:password_length).returns(6..512)
97+
password = '🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠'
98+
user = new_user(password: password, password_confirmation: password)
99+
assert user.invalid?
100+
assert_equal 'too long (maximum is 72 bytes)', user.errors[:password].join
101+
end
102+
95103
test 'should not require password length when it\'s not changed' do
96104
user = create_user.reload
97105
user.password = user.password_confirmation = nil

0 commit comments

Comments
 (0)