diff options
Diffstat (limited to 'lib/security/weak_passwords.rb')
-rw-r--r-- | lib/security/weak_passwords.rb | 88 |
1 files changed, 88 insertions, 0 deletions
diff --git a/lib/security/weak_passwords.rb b/lib/security/weak_passwords.rb new file mode 100644 index 00000000000..42b02132933 --- /dev/null +++ b/lib/security/weak_passwords.rb @@ -0,0 +1,88 @@ +# 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 |