# 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