Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ en:
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"
password_too_long_for_bcrypt: "too long (maximum is 72 bytes)"
7 changes: 7 additions & 0 deletions devise.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,11 @@ Gem::Specification.new do |s|
s.add_dependency("bcrypt", "~> 3.0")
s.add_dependency("railties", ">= 7.0")
s.add_dependency("responders")

s.post_install_message = %q{
[DEVISE] Devise now strictly enforces a 72-byte limit on passwords.
This prevents a known BCrypt security issue where passwords exceeding 72 bytes are silently truncated, potentially causing hash collisions.

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.
}
end
2 changes: 1 addition & 1 deletion lib/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ module Test

# Range validation for password length
mattr_accessor :password_length
@@password_length = 6..128
@@password_length = 6..72 # max 72 bytes for bcrypt

# The time the user will be remembered without asking for credentials again.
mattr_accessor :remember_for
Expand Down
17 changes: 16 additions & 1 deletion lib/devise/models/validatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module Models
# Validatable adds the following options to +devise+:
#
# * +email_regexp+: the regular expression used to validate e-mails;
# * +password_length+: a range expressing password length. Defaults to 6..128.
# * +password_length+: a range expressing password length. Defaults to 6..72.
#
# Since +password_length+ is applied in a proc within `validates_length_of` it can be overridden
# at runtime.
Expand All @@ -21,6 +21,9 @@ module Validatable
VALIDATIONS = [:validates_presence_of, :validates_uniqueness_of, :validates_format_of,
:validates_confirmation_of, :validates_length_of].freeze

# maximum allowed bytes for BCrypt (72 bytes)
MAX_PASSWORD_BCRYPT_LENGTH_ALLOWED = 72

def self.required_fields(klass)
[]
end
Expand All @@ -37,6 +40,8 @@ def self.included(base)
validates_presence_of :password, if: :password_required?
validates_confirmation_of :password, if: :password_required?
validates_length_of :password, minimum: proc { password_length.min }, maximum: proc { password_length.max }, allow_blank: true

validate :max_password_length_for_bcrypt
end
end

Expand All @@ -62,6 +67,16 @@ def email_required?
true
end

# Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes)
def max_password_length_for_bcrypt
if password.present?
password_already_too_long = self.errors.where(:password, :too_long).present?
if !password_already_too_long && password.bytesize > MAX_PASSWORD_BCRYPT_LENGTH_ALLOWED
self.errors.add(:password, :password_too_long_for_bcrypt)
end
end
end

module ClassMethods
Devise::Models.config(self, :email_regexp, :password_length)
end
Expand Down
4 changes: 2 additions & 2 deletions lib/generators/templates/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@
# config.rememberable_options = {}

# ==> Configuration for :validatable
# Range for password length.
config.password_length = 6..128
# Range for password length. 72 bytes max for bcrypt
config.password_length = 6..72

# Email regex used to validate email formats. It simply asserts that
# one (and only one) @ exists in the given string. This is mainly
Expand Down
8 changes: 8 additions & 0 deletions test/models/validatable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ class ValidatableTest < ActiveSupport::TestCase
assert_equal 'is too long (maximum is 72 characters)', user.errors[:password].join
end

test 'should validate that password cannot be bigger that 72 bytes for bcrypt' do
Devise.stubs(:password_length).returns(6..512)
password = '🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠🫠'
user = new_user(password: password, password_confirmation: password)
assert user.invalid?
assert_equal 'too long (maximum is 72 bytes)', user.errors[:password].join
end

test 'should not require password length when it\'s not changed' do
user = create_user.reload
user.password = user.password_confirmation = nil
Expand Down