Protecting A Django App From Password Guessing Attacks
Welcome to my fourth article in the series ‘OWASP Top-10 From A Python/Django Perspective’. My previous three articles were on the first OWASP vulnerability - Injection (A1) and covered SQL, LDAP and OS Injection. The next few articles are for the second vulnerability - Broken Authentication (A2). In this article we will learn how to protect a Django application from password guessing attacks.
What Is A Password Guessing Attack?
Password Guessing is a kind of attack where a malicious user, using automated tools, guesses different credentials consecutively with the intent to take over an account. There are different kinds of password guessing attacks, but the two most common & effective ones are:
- Dictionary Attack
- Credential Stuffing
In a Dictionary Attack, the malicious entity uses a dictionary (generally a text file) containing a huge number of words that can be used as passwords for the particular account. The password cracking tool (such as THC Hydra) will pass each word in the dictionary to the application. If any word in the dictionary matches the password, the malicious user will be authenticated. Usually, the dictionary would contain the most commonly used passwords and words that are custom created for the target.
In a Credential Stuffing Attack, the malicious user injects breached usernames and passwords to the application. There are millions of username and password combinations available from previous breaches which can be used in this attack. This technique is effective as many users tend to reuse a username and password across multiple applications for convenience.
How To Protect Against Password Guessing Attacks?
A Django app can be protected against a Password Guessing Attack by implementing the following controls.
- Good Password Policy
- Account Lockout
- Multi Factor Authentication
Good Password Policy
Traditionally, we consider complex passwords as good passwords. The password policy is defined in such a way that mandates the users to create very complex passwords. An example of such a password policy is:
Password should:
- Contain at least 8 characters
- Contain at least one lower case character
- Contain at least one upper case character
- Contain at least one number
- Contain at least one special character
The problem with such a policy is that it frequently leads users to create passwords that are ‘hard to remember for the user, but easy to guess for an attacker’. For example, ‘Admin@123456’ is a valid password for the above policy. But is the password Good? Not really!
Additionally, users have so many accounts that it becomes practically difficult to remember all of them. This forces the user to reuse a password across multiple accounts.
All these have lead people to realize that a ‘Complex Password’ does not necessarily mean a ‘Good Password’. Taking this into consideration, NIST has created a new set of guidelines called NIST Special Publication 800-63B.
The guideline specifies that a password:
- May contain special characters. But it is NOT MANDATORY to contain special characters
- Should contain at least 8 characters. It is however suggested to make the password AS LONG as practically possible.
- Should not contain sequential and repetitive characters (e.g. 12345 or aaaaa)
- Should not be context-specific (e.g. username, company name, website name and derivatives thereof)
- Should not be a password that is commonly used
- Should not be a password obtained from previous breach corpuses
It is clear that the guideline is focussed on creating passwords that are ‘long and easy to remember for the user, but hard to guess for an attacker’. It will also block bad passwords from being created.
For e.g. the password ‘Admin@123456’ which is a valid password as per our traditional policy will be rejected by multiple conditions above. The password contains sequential characters 123456 and thus condition ‘3’ above will block creation of the password. If the username is admin, condition ‘4’ will come into effect. Finally, the password is so common that condition ‘5’ and ‘6’ will also most likely block it.
However, a password such as ‘teddy-bears-are-so-cute-i-love-them-so-much’ is very long, easy to remember but hard to guess for anyone else. This is a secret that only I know. Even if someone else knew that I love teddy bears, it would be very difficult to correctly guess and construct the long sentence. Moreover, I like and hate a lot of other things. I could use any of that as the theme for my password. All of these combined makes guessing and constructing the long password very hard. The application will only block the password from being created if it is found to be already breached.
This also makes remembering multiple passwords a bit easier and will help in preventing password reuse.
Now let’s understand how to implement the above NIST password guideline in Django.
Django provides a mechanism for validating passwords. There are few default validators that we can use. But if we want to perform additional password validation, we can write our own validators. The validators should be included in the AUTH_PASSWORD_VALIDATORS list in settings.py.
May contain special characters. But it is NOT MANDATORY to contain special characters
For this, we need not do anything. Django, by default, will accept special characters. The important thing which we are doing here is breaking away from the traditional practice of mandating special characters in the password.
Should contain at least 8 characters. It is however suggested to make the password AS LONG as practically possible.
Django’s MinimumLengthValidator can be used to implement this condition. Add the following in the AUTH_PASSWORD_VALIDATORS list.
'OPTIONS': {'min_length':8} define the minimum length of the password.
The maximum number of characters allowed in a password in Django is 4096. This is more than enough and gives users the freedom to create long but practical passwords.
Should not contain sequential and repetitive characters (e.g. 12345 or aaaaa)
I could not find an in-built validator or an existing validator on the internet for this condition. So I wrote my own validator for this.
Create a validators.py file within the application folder. Validators are basically classes that needs to have at least two methods - ‘validate’ and ‘get_help_text’. The validate method performs the validation and will raise an error in case the condition is not met. The get_help_text method define the help message.
For this particular condition I created three validator classes:
- ConsecutivelyRepeatingCharacterValidator
This validator checks if the password contains characters that are repeating consecutively (e.g. 1111 or aaaa).
The first method __init__ would give us the minimum number of times a character can repeat consecutively. By default, the ‘length’ is 3, which means that passwords containing characters that are repeated at least three times consecutively would be rejected. E.g. ‘password-aaa’ would be rejected since the character ‘a’ repeats consecutively 3 times. ‘Password11111’ would be rejected since the character ‘1’ repeats consecutively 5 times. We can define the minimum ‘length’ while using the validator in settings.py using OPTIONS.
In the above case, I have defined the length as 5 which means that passwords such as ‘22222admin’ and ‘jerrrrrrin’ would be rejected.
When a password is rejected, a validation error 'The password contains characters that are consecutively repeating. e.g 1111 or aaaa' would be raised.
- ConsecutivelyIncreasingIntegerValidator
This validator checks if the password contains consecutively increasing integers such as 123456 or 6789.
The first method __init__ would give us the minimum number of times an integer can sequentially increase. By default, the ‘length’ is 3, which means that passwords containing integers that are at least three digits long (e.g. 123) and the individual integers (e.g. 1, 2 and 3) are consecutively and sequentially increasing would be rejected. For e.g. admin@123 would be rejected as the integer 123 is three digits long and contains individual characters 1, 2 and 3 that are consecutively and sequentially increasing. Pass0123456 would also be rejected as the integer 0123456 is more than three digits long and is consecutively and sequentially increasing. Please note that password such as admin@15764 would not be rejected by this validator. We can define the minimum ‘length’ while using the validator in settings.py using OPTIONS.
In the above case, I have defined the length as 4 which means that passwords such as ‘123456admin’ and ‘ad123456min’ would be rejected.
When a password is rejected, a validation error 'The password contains consecutively increasing integers. e.g 12345' would be raised.
- ConsecutivelyDecreasingIntegerValidator
This validator is similar to the previous validator with the difference that this checks for consecutively decreasing integers such as 654321 and 9876.
We can define the minimum length while using the validator in settings.py using OPTIONS.
The above three validator classes together will implement the required condition.
Should not be context-specific (e.g. containing username, company name, website name and derivatives thereof)
We can implement this condition using the in-built UserAttributeSimilarityValidator and a custom validator.
The UserAttributeSimilarityValidator will validate if the password is similar to other information about the user such as username, first name, last name and email.
A custom validator can be written to check if the password contains other words such as company/website name and the derivatives of it.
I created a list called ‘context’ that contains derivatives of my company name. You can create your own custom list as per your company/website name. A good way to get possible derivatives of a word is to use a python script called Common User Passwords Profiler (CUPP) created by Mebus.
Then add the ContextValidator in the AUTH_PASSWORD_VALIDATORS list.
You can also consider creating a text file which will contain the derivatives and then reading from the file and validating.
Should not be a password that is commonly used
Django provides an in-built validator called CommonPasswordValidator that will check against the top 20000 passwords created by Royce Williams.
Should not be a password obtained from previous breach corpuses
For implementing this condition, I found a custom validator called PwnedPasswordsValidator that was created by James Bennett. It checks the password against the Pwned Password database of 555,278,657 (as of the time of writing) real world passwords previously exposed in data breaches. The database is maintained by Troy Hunt who is generous enough to provide an API to connect to it. More details about the API and how it works can be found here.
Step 1: Install the pwned-passwords-django package using pip.
pip install pwned-passwords-django
Step 2: Add ‘pwned_passwords_django.validators.PwnedPasswordsValidator’ as the first validator in the AUTH_PASSWORD_VALIDATORS list.
Step 3: Add ‘pwned_passwords_django.middleware.PwnedPasswordsMiddleware’ to end of the MIDDLEWARE settings.
Now that you have a modern password policy, let’s move on to the next control.
Account Lockout
Locking accounts after a pre-defined number of failed login attempts may be one of the most implemented controls to prevent a password guessing attack. Most critical web applications such as Internet Banking locks users out after 3 to 5 failed logins and allows login only after a specific amount of time.
To implement this, there is a package called django-axes created by Jazzband. Follow the steps below:
Step 1 - Install django-axes
pip install django-axes
Step 2 - Add ‘axes’ to INSTALLED_APPS
Step 3 - Add AUTHENTICATION_BACKENDS as below in settings.py
Step 4 - Add ‘axes.middleware.AxesMiddleware’ to the end of MIDDLEWARE
Step 5 - Run 'python manage.py check' to ensure all configurations are okay
Step 6 - Run 'python manage.py migrate' to create the axes databases
Step 7 - Add the following in settings.py
‘AXES_FAILURE_LIMIT=10’ will lock the user after 10 failed logins.
‘AXES_COOLOFF_TIME = timedelta(minutes=10)’ will allow the user to login after 10 minutes of lockout.
‘AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP = True’ will use both Username and IP to identify a person. Using only Username may allow malicious entities to lockout genuine users by repeatedly sending false credentials. Using only IP may block all users within that IP (e.g. in a company).
‘AXES_LOCKOUT_TEMPLATE’ is the custom template that will be displayed on lockout.
Note
To use timedelta import from datetime.
from datetime import timedelta
Multi Factor Authentication
Multi Factor Authentication (MFA) simply means multiple levels of user verification. Most of the time we use only the username and password to verify whether the user is indeed the right one. MFA adds another verification layer by asking the user to input an OTP received through SMS or displayed in a mobile application such as Google Authenticator. There are also other mechanism such as asking the user to insert a special USB that is being assigned to him. MFA helps in cases where the Username and Password is breached. It makes the job of the attacker difficult as he would need to additionally have your mobile phone / special USB.
There is a package called django-mfa, created by Micro Pyramid, which we will use.
Step 1 - Install django-mfa
pip install django-mfa
Step 2 - Add ‘django-mfa’ to the list of INSTALLED_APPS in settings.py
Step 3 - Add 'django_mfa.middleware.MfaMiddleware' in the MIDDLEWARE list in settings.py
Step 4 - Add the following to the root urls.py file
Step 5 - Add a link for MFA Settings
For a logged in user, provide a link to setup MFA. The link must point to /mfa-settings/security/.
Once the logged-in user clicks on the link, he will be taken to the MFA setup page.
The user can now setup the MFA using an app such as Google Authenticator.
This is just an example of how MFA can be implemented. If you want to create your own MFA system with custom views and features, you can explore the django-otp package created by Peter Sagerson.
Full Stack DevOps Engineer (Team Lead)
2 å¹´Thanks so much for this
Data Engineer
4 å¹´Thank you for your awesome post