forked from decidim/decidim
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update password strength check (decidim#8455)
- Loading branch information
Showing
17 changed files
with
128,812 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
# frozen_string_literal: true | ||
|
||
# Class is used to verify that the user's password is strong enough | ||
class PasswordValidator < ActiveModel::EachValidator | ||
MINIMUM_LENGTH = 10 | ||
MAX_LENGTH = 256 | ||
MIN_UNIQUE_CHARACTERS = 5 | ||
IGNORE_SIMILARITY_SHORTER_THAN = 4 | ||
VALIDATION_METHODS = [ | ||
:password_too_short?, | ||
:password_too_long?, | ||
:not_enough_unique_characters?, | ||
:name_included_in_password?, | ||
:nickname_included_in_password?, | ||
:email_included_in_password?, | ||
:domain_included_in_password?, | ||
:password_too_common?, | ||
:blacklisted? | ||
].freeze | ||
|
||
# Check if user's password is strong enough | ||
# | ||
# record - Instance of a form (e.g. Decidim::RegistrationForm) or model | ||
# attribute - "password" | ||
# value - Actual password | ||
# Returns true if password is strong enough | ||
def validate_each(record, attribute, value) | ||
return false if value.blank? | ||
|
||
@record = record | ||
@attribute = attribute | ||
@value = value | ||
@weak_password_reasons = [] | ||
|
||
return true if strong? | ||
|
||
@weak_password_reasons.each do |reason| | ||
record.errors[attribute] << get_message(reason) | ||
end | ||
|
||
false | ||
end | ||
|
||
private | ||
|
||
attr_reader :record, :attribute, :value | ||
|
||
def get_message(reason) | ||
I18n.t "password_validator.#{reason}" | ||
end | ||
|
||
def strong? | ||
VALIDATION_METHODS.each do |method| | ||
@weak_password_reasons << method.to_s.sub(/\?$/, "").to_sym if send(method.to_s) | ||
end | ||
|
||
@weak_password_reasons.empty? | ||
end | ||
|
||
def password_too_short? | ||
value.length < MINIMUM_LENGTH | ||
end | ||
|
||
def password_too_long? | ||
value.length > MAX_LENGTH | ||
end | ||
|
||
def not_enough_unique_characters? | ||
value.chars.uniq.length < MIN_UNIQUE_CHARACTERS | ||
end | ||
|
||
def name_included_in_password? | ||
return false if !record.respond_to?(:name) || record.name.blank? | ||
return true if value.include?(record.name.delete(" ")) | ||
|
||
record.name.split(" ").each do |part| | ||
next if part.length < IGNORE_SIMILARITY_SHORTER_THAN | ||
|
||
return true if value.include?(part) | ||
end | ||
|
||
false | ||
end | ||
|
||
def nickname_included_in_password? | ||
return false if !record.respond_to?(:nickname) || record.nickname.blank? | ||
|
||
value.include?(record.nickname) | ||
end | ||
|
||
def email_included_in_password? | ||
return false if !record.respond_to?(:email) || record.email.blank? | ||
|
||
name, domain, _whatever = record.email.split("@") | ||
value.include?(name) || (domain && value.include?(domain.split(".").first)) | ||
end | ||
|
||
def domain_included_in_password? | ||
return false unless record&.current_organization&.host | ||
return true if value.include?(record.current_organization.host) | ||
|
||
record.current_organization.host.split(".").each do |part| | ||
next if part.length < IGNORE_SIMILARITY_SHORTER_THAN | ||
|
||
return true if value.include?(part) | ||
end | ||
|
||
false | ||
end | ||
|
||
def blacklisted? | ||
Array(Decidim.password_blacklist).each do |expression| | ||
return true if expression.is_a?(Regexp) && value.match?(expression) | ||
return true if expression.to_s == value | ||
end | ||
|
||
false | ||
end | ||
|
||
def password_too_common? | ||
Decidim::CommonPasswords.instance.passwords.include?(value) | ||
end | ||
end |
2 changes: 1 addition & 1 deletion
2
decidim-core/app/views/decidim/account/_password_fields.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
<%= form.password_field :password, value: form.object.password, autocomplete: "off", help_text: t("devise.passwords.edit.password_help", minimun_characters: NOBSPW.configuration.min_password_length) %> | ||
<%= form.password_field :password, value: form.object.password, autocomplete: "off", help_text: t("devise.passwords.edit.password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH) %> | ||
<%= form.password_field :password_confirmation, value: form.object.password_confirmation, autocomplete: "off" %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# frozen_string_literal: true | ||
|
||
module Decidim | ||
class CommonPasswords | ||
include Singleton | ||
|
||
attr_reader :passwords | ||
|
||
URLS = %w( | ||
https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/xato-net-10-million-passwords-1000000.txt | ||
https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/darkweb2017-top10000.txt | ||
https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt | ||
).freeze | ||
|
||
def initialize | ||
raise FileNotFoundError unless File.exist?(self.class.common_passwords_path) | ||
|
||
File.open(self.class.common_passwords_path, "r") do |file| | ||
@passwords = file.read.split | ||
end | ||
end | ||
|
||
def self.update_passwords! | ||
File.open(common_passwords_path, "w") do |file| | ||
common_password_list.each { |item| file.puts(item) } | ||
end | ||
end | ||
|
||
def self.common_password_list | ||
@common_password_list ||= begin | ||
list = [] | ||
URLS.each do |url| | ||
URI.open(url) do |data| | ||
data.read.split.each do |line| | ||
list << line if line.length >= min_length | ||
end | ||
end | ||
end | ||
|
||
list.uniq | ||
end | ||
end | ||
|
||
def self.min_length | ||
return ::PasswordValidator::MINIMUM_LENGTH if defined?(::PasswordValidator) | ||
|
||
10 | ||
end | ||
|
||
def self.common_passwords_path | ||
File.join(__dir__, "db", "common-passwords.txt") | ||
end | ||
|
||
class FileNotFoundError < StandardError; end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.