Django 1.9 "Common Password Validator" - Strange Behaviour - django

I'm attempting to replace the built in common-passwords.txt.gz file, which supposedly contains the top 1,000 common passwords, with my own identical version which contains the top 10,000 common passwords for my country, but I've encountered some rather strange behaviour.
Firstly I directly substituted Django's common-passwords.txt.gz file (4KB) with my own containing my .txt file with the same utf-8 encoding as Django (which comes in at 34KB), then restarted the test server. When changing a users password to "password" it does not raise the expected error as it does with Django's common password file.
The first line of both the built in password list and my new one begins 123456password12345678qwerty123456789... so it clearly should do.
When I append a few extra passwords to their common-passwords file it appears to work as it should and raise an error if I try to use them as passwords, so I don't think that it's cached somewhere or anything like that.
Is there some kind of built in file size limit for the common password list or for the gzip.open(password_list_path).read().decode('utf-8').splitlines() function?
Secondly, trying to figure out the above led me to a strange bug. Using Django's built in common-passwords.txt.gz (of which the first line starts 123456password12345678qwerty123456789...) successfully raises a validation error for "password" and "password1", but not for "password12" or "password123"!
As I read it, the Django validation code basically checks if the submitted password is in each line from the common passwords file, and I cannot find any code that exempts passwords above a certain length from the validation. Am I missing something or is this a bug?
The "common password validation" function in Django 1.9 is found in \venv\Lib\site-packages\django\contrib\auth\password_validation.py, the relevant class is below:
class CommonPasswordValidator(object):
"""
Validate whether the password is a common password.
The password is rejected if it occurs in a provided list, which may be gzipped.
The list Django ships with contains 1000 common passwords, created by Mark Burnett:
https://xato.net/passwords/more-top-worst-passwords/
"""
DEFAULT_PASSWORD_LIST_PATH = os.path.join(
os.path.dirname(os.path.realpath(upath(__file__))), 'common-passwords.txt.gz'
)
def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH):
try:
common_passwords_lines = gzip.open(password_list_path).read().decode('utf-8').splitlines()
except IOError:
with open(password_list_path) as f:
common_passwords_lines = f.readlines()
self.passwords = {p.strip() for p in common_passwords_lines}
def validate(self, password, user=None):
if password.lower().strip() in self.passwords:
raise ValidationError(
_("This password is too common (it would be trivial to crack!)"),
code='password_too_common',
)
def get_help_text(self):
return _("Your password can't be a commonly used password.")

Finally got to the bottom of this!
There is some kind of invisible unrendered character in-between the passwords contained in Django's built in common passwords validation file, this explains both issues I encountered.
I changed my top 10k common passwords file to have the usual newline characters between them instead and now it all works great! Even though there are now 10 times as many passwords for it to compare against it still runs pretty much instantaneously!
I've uploaded my 10,000 most common passwords file to github for any future people who encounter this issue or who just want to improve Django's built-in common password validation: https://github.com/timboss/Django-Common-Password-Validation/

Related

What are the differences between using self.add_error() and raising a ValidationError()?

You can throw a validation error in 2 ways. The first is with self.add_error() and the second with raise ValidationError().
I've read that when you use self.add_error('field1','description here') then field1 is also automatically removed from the cleaned_data list and i assume a ValidationError object is also added to the self.errors list, is this correct?
But what happens when you don't choose to use self.add_error and opt for using raise ValidationError instead? Is this object also automatically added to the errors list behind the scenes? And how would you display this error message as caption under the correct invalid field?
Thank you
If you raise an error, the control flow of that method, and callers of that method stops, until there is a method that has span a try-except over that, and catches the exception accordingly.
But sometimes a field might contain multiple errors. For example if you have a password, you might want to add errors because it is too short, does not contain a digit, a lowercase, and/or upppercase.
Then you thus can implement this with:
def clean_password(self):
pwd = self.cleaned_data['password']
if len(pwd) < 10:
self.add_error('password', 'The password is too short.')
if not any(c.isupper() for c in pwd):
self.add_error('password', 'The password should contain an uppercase character.')
if not any(c.islower() for c in pwd):
self.add_error('password', 'The password should contain an lowercase character.')
if not any(c.isdigit() for c in pwd):
self.add_error('password', 'The password should contain an digit.')
return pwd
If you would raise a ValidationError for one of these, it can not add mutliple problems that a password might have.
You can however pass a list of errors to a ValidationError data constructor, hence you can use the two interchangeable.

Django forms EmailValidation not working

I have been researching on this issue but it seems there's not a lot of explanation around there covering this.
...
class RangerRegistrationForm(RegistrationFormUniqueEmail):
email = forms.EmailField(label=_("Email Address"), validators=[EmailValidator(whitelist=['gmail.com'])])
...
Here's the part of my script where I check if the user supplies a gmail account. Unfortunately, as long as it's a valid email it will always pass the check.
What am I doing wrong here?
This is NOT a bug in Django (re-read the source code link posted in #catavaran's answer).
A whitelist in this case is not a "block everything except for this domain part" solution. Rather, the whitelist is a domain part that would otherwise be flagged as invalid by Django's EmailValidator.
For example, the default whitelist is set to domain_whitelist = ['localhost']...an otherwise invalid domain_part that is being flagged as being OK for this use case.
To validate the domain part of an email field, you are going to need to write your own clean function. Something like:
class RangerRegistrationForm(forms.Form):
email = forms.EmailField(label=_("Email Address"))
def clean_email(self):
submitted_data = self.cleaned_data['email']
if '#gmail.com' not in submitted_data:
raise forms.ValidationError('You must register using a Gmail address')
return submitted_data
Congratulations! You had found a bug in Django.
Look at this code from the EmailValidator:
if (domain_part not in self.domain_whitelist and
not self.validate_domain_part(domain_part)):
...
If the domain part of the e-mail is valid then checking against the self.domain_whitelist just ignored.

user authentication script to run program using python

i am new on python and just started learning, i want to create a simple script to run a program by authenticating user, valid or not...
main problem, i am facing is that i have a file "users" in which i mentioned all valid users,
so i need a program which searches the input given by user in "users" file, if found then continue otherwise "authentication failed"
what i tried is ...
fp = open("users", "r")
for line in fp.readlines():
if "michel" in line : # user michel is available in users file
print "true"
else :
print "false"
it works, prints 'true' because michel is in file.. but in case of when user enters "mic" .. also prints true ...... so what will be the solution...
For starters, it is probably best for security purposes to use os.getlogin() to determine the user's login name rather than prompting the user to type their username. This will at least guarantee that the user logged in via some authentication mechanism to get onto the system, meaning that they have a known & consistent username.
So if you wanted to turn this into a function you could write:
def is_valid_user(username):
fp = open("users", "r")
for line in fp.readlines():
if username in line:
fp.close()
return True
fp.close()
return False
You could then call the function using:
import os
is_valid = is_valid_user(os.getlogin())
if is_valid:
print("valid user")
else:
print("invalid user")
Some suggestions for added security now and in the future:
Modify your "users" file to contain names surrounded by delimiters such as ":jonesj:" rather than "jonesj" and search for ":" + username + ":" in line which will avoid false positives in situations where a user "jones" is currently logged in and a username "jonesj" is in your "users" file but "jones" is not, and you incorrectly identify "jones" as being an authorized user (since "jones" is a subset of the string "jonesj").
Make sure the permissions on your "users" file is set to read-only so that users can't go add their username to the file to grant permissions to themselves.
Sometime in the future you may want to consider using LDAP or Kerberos server or some other more formal authentication mechanism rather than a "users" file. There are good python client libraries for quite a number of authentication backend servers.
You can use re to make sure the whole line is a match:
import re
fp = open("users", "r")
for line in fp.readlines():
if re.match('^michel$', line, re.I):
print "true"
else :
print "false"

Django-registation : Error on activate new user

I'm using Django registration inside my project on a development server.
When I register a new user, I use EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' to get the activation link.
When I try to put the activation link into the web browser, I have an error, and the account is not activated.
It is said :
Thank you.
This function is used to generate the key.
def create_profile(self, user):
"""
Create a ``RegistrationProfile`` for a given
``User``, and return the ``RegistrationProfile``.
The activation key for the ``RegistrationProfile`` will be a
SHA1 hash, generated from a combination of the ``User``'s
username and a random salt.
"""
salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
username = user.username
if isinstance(username, unicode):
username = username.encode('utf-8')
activation_key = hashlib.sha1(salt+username).hexdigest()
return self.create(user=user,
activation_key=activation_key)
I received that mail. But I use EMAIL_BACKEND'django.core.mail.backends.filebased.EmailBackend'.
I think the problem comes from here. But I can't test in production server.
I solved the problem actually It's because I generate the email to send inside a file thanks to the file email backends provided by django for development purpose. Inside this file, when there is a carriage return, it adds an = characters. And this is the case with the link to active the account.
You shouldn't have a = character in your activation key.
Although sergzach's answer will work, I'd be more interested in finding out why that = is there in the first place.
django-registration usually generates the key as follows:
salt = sha.new(str(random.random())).hexdigest()[:5]
activation_key = sha.new(salt+user.username).hexdigest()
Where are you generating yours?
The character '=' is not in the range of \w+. Use [\w=]+ instead of \w+.
Replace ?P<activation_key>\w+ to ?P<activation_key>[\w=]+

Understanding User class in django

I create a user in my view.py using this simple code.
if not errors:
user = User.objects.create_user(username, email, password)
user.save()
Except for the validation, there is nothing that I do to the username and password values before creating the object.
But I find this in the User class in Django API. I don't know how to use the help text. If it is help text what does it print? How do I find the default values of algo, salt and hexdigest?
password = models.CharField(_('password'), max_length=128, help_text=_("Use '[algo]$[salt]$[hexdigest]' or use the change password form."))
"If it is help text what does it print?"
-> it prints exactly this: Use '[algo]$[salt]$[hexdigest]'
when you create a user, it will automatically call make_password(password[, salt, hashers])
which: Creates a hashed password in the format used by this application. It takes one mandatory argument: the password in plain-text. Optionally, you can provide a salt and a hashing algorithm to use, if you don't want to use the defaults (first entry of PASSWORD_HASHERS setting). Currently supported algorithms are: 'pbkdf2_sha256', 'pbkdf2_sha1', 'bcrypt' (see Using bcrypt with Django), 'sha1', 'md5', 'unsalted_md5'
are you facing any problems with this?
create_user will automatically generate password hash and it will create user in the database (thus you don't need that user.save())
See docs on creating users.
The help text is basicly just code for the message that shows up in the django admin, when editing a User object. It's meant to explain to someone looking at the edit form, why the password field has something like sha1$12345$1234567890abcdef1234567890abcdef12345678 instead of the password that was set for that user. The reason is, of course that the password is hashed for security, and that representation holds all the information required to verify a user-typed password later.
The admin user edit form has a special page for editing passwords. If you want to edit the users password in your code use the set_password method of the User object, the check_password method is for verifying a supplied password.
The documentation for make_password has more information about the algorithms Django uses and can use. The default for Django <1.3 was sha1, Django 1.4 changed the default to PBKDF2. The default value for salt is a random string (it's there so that two identical passwords don't look the same in the database). Hexdigest is the value of the password string and the salt string hashed with the hashing algorithm. You can read the details in the code on github.