debian-mirror-gitlab/lib/security/weak_passwords.rb

101 lines
3.6 KiB
Ruby
Raw Normal View History

2022-10-11 01:57:18 +05:30
# frozen_string_literal: true
module Security
module WeakPasswords
# These words are predictable in GitLab's specific context, and
# therefore cannot occur anywhere within a password.
FORBIDDEN_WORDS = Set['gitlab', 'devops'].freeze
# Substrings shorter than this may appear legitimately in a truly
# random password.
MINIMUM_SUBSTRING_SIZE = 4
2023-03-04 22:38:38 +05:30
# Passwords of 64+ characters are more likely to randomly include a
# forbidden substring.
#
# This length was chosen somewhat arbitrarily, balancing security,
# usability, and skipping checks on `::User.random_password` which
# is 128 chars. See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105755
PASSWORD_SUBSTRING_CHECK_MAX_LENGTH = 64
2022-10-11 01:57:18 +05:30
class << self
# Returns true when the password is on a list of weak passwords,
# or contains predictable substrings derived from user attributes.
# Case insensitive.
def weak_for_user?(password, user)
forbidden_word_appears_in_password?(password) ||
name_appears_in_password?(password, user) ||
username_appears_in_password?(password, user) ||
email_appears_in_password?(password, user) ||
password_on_weak_list?(password)
end
private
def forbidden_word_appears_in_password?(password)
contains_predicatable_substring?(password, FORBIDDEN_WORDS)
end
def name_appears_in_password?(password, user)
return false if user.name.blank?
# Check for the full name
substrings = [user.name]
# Also check parts of their name
substrings += user.name.split(/[^\p{Alnum}]/)
contains_predicatable_substring?(password, substrings)
end
def username_appears_in_password?(password, user)
return false if user.username.blank?
# Check for the full username
substrings = [user.username]
# Also check sub-strings in the username
substrings += user.username.split(/[^\p{Alnum}]/)
contains_predicatable_substring?(password, substrings)
end
def email_appears_in_password?(password, user)
return false if user.email.blank?
# Check for the full email
substrings = [user.email]
# Also check full first part and full domain name
substrings += user.email.split("@")
# And any parts of non-word characters (e.g. firstname.lastname+tag@...)
substrings += user.email.split(/[^\p{Alnum}]/)
contains_predicatable_substring?(password, substrings)
end
def password_on_weak_list?(password)
# Our weak list stores SHA2 hashes of passwords, not the weak
# passwords themselves.
digest = Digest::SHA256.base64digest(password.downcase)
Settings.gitlab.weak_passwords_digest_set.include?(digest)
end
# Case-insensitively checks whether a password includes a dynamic
# list of substrings. Substrings which are too short are not
# predictable and may occur randomly, and therefore not checked.
2023-03-04 22:38:38 +05:30
# Similarly passwords which are long enough to inadvertently and
# randomly include a substring are not checked.
2022-10-11 01:57:18 +05:30
def contains_predicatable_substring?(password, substrings)
2023-03-04 22:38:38 +05:30
return unless password.length < PASSWORD_SUBSTRING_CHECK_MAX_LENGTH
2022-10-11 01:57:18 +05:30
substrings = substrings.filter_map do |substring|
substring.downcase if substring.length >= MINIMUM_SUBSTRING_SIZE
end
password = password.downcase
# Returns true when a predictable substring occurs anywhere
# in the password.
substrings.any? { |word| password.include?(word) }
end
end
end
end