gitlab-org--gitlab-foss/lib/security/weak_passwords.rb

88 lines
3.1 KiB
Ruby

# 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
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.
def contains_predicatable_substring?(password, substrings)
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