The improper storage of passwords poses a significant security risk to software applications. This vulnerability arises when passwords are stored in plaintext or with a fast hashing algorithm. To exploit this vulnerability, an attacker typically requires access to the stored passwords.
Attackers who would get access to the stored passwords could reuse them without further attacks or with little additional effort.
Obtaining the
plaintext passwords, they could then gain unauthorized access to user accounts, potentially leading to various malicious activities.
Plaintext or weakly hashed password storage poses a significant security risk to software applications.
When passwords are stored in plaintext or with weak hashing algorithms, an attacker who gains access to the password database can easily retrieve and use the passwords to gain unauthorized access to user accounts. This can lead to various malicious activities, such as unauthorized data access, identity theft, or even financial fraud.
Many users tend to reuse passwords across multiple platforms. If an attacker obtains plaintext or weakly hashed passwords, they can potentially use these credentials to gain unauthorized access to other accounts held by the same user. This can have far-reaching consequences, as sensitive personal information or critical systems may be compromised.
Many industries and jurisdictions have specific regulations and standards to protect user data and ensure its confidentiality. Storing passwords in plaintext or with weak hashing algorithms can lead to non-compliance with these regulations, potentially resulting in legal consequences, financial penalties, and damage to the reputation of the software application and its developers.
from argon2 import PasswordHasher, profiles
def hash_password(password):
ph = PasswordHasher.from_parameters(profiles.CHEAPEST) # Noncompliant
return ph.hash(password)
from argon2 import PasswordHasher
def hash_password(password):
ph = PasswordHasher()
return ph.hash(password)
In general, the default values of the Argon2 library are considered safe. If you need to change the parameters, you should note the following:
First, Argon2 comes in three forms: Argon2i, Argon2d and Argon2id. Argon2i is optimized for hashing passwords and uses data-independent memory
access. Argon2d is faster and uses data-dependent memory access, making it suitable for applications where there is no risk of side-channel
attacks.
Argon2id is a mixture of Argon2i and Argon2d and is recommended for most applications.
Argon2id has three different parameters that can be configured: the basic minimum memory size (m), the minimum number of iterations (t) and the
degree of parallelism (p).
The higher the values of m, t and p result in safer hashes, but come at the cost of higher resource usage. There exist
general recommendations that balance security and speed in an optimal way.
Hashes should be at least 32 bytes long and salts should be at least 16 bytes long.
Next, the recommended parameters for Argon2id are:
| Recommendation type | Time Cost | Memory Cost | Parallelism |
|---|---|---|---|
| Argon2 Creators |
1 |
2097152 (2 GiB) |
4 |
| Argon2 Creators |
3 |
65536 (64 MiB) |
4 |
| OWASP |
1 |
47104 (46 MiB) |
1 |
| OWASP |
2 |
19456 (19 MiB) |
1 |
| OWASP |
3 |
12288 (12 MiB) |
1 |
| OWASP |
4 |
9216 (9 MiB) |
1 |
| OWASP |
5 |
7168 (7 MiB) |
1 |
To use values recommended by the Argon2 authors, you can use the following objects:
To use values recommended by the OWASP you can craft objects as follows:
from argon2 import Parameters
from argon2.low_level import ARGON2_VERSION, Type
OWASP_1 = argon2.Parameters(
type=Type.ID,
version=ARGON2_VERSION,
salt_len=16,
hash_len=32,
time_cost=1,
memory_cost=47104, # 46 MiB
parallelism=1)
def hash_password(password):
ph = PasswordHasher.from_parameters(OWASP_1)
return ph.hash(password)
To determine which one is the most appropriate for your application, you can use the argon2 CLI, for example with OWASP’s first recommendation:
$ pip install argon2 $ python -m argon2 -t 1 -m 47104 -p 1 -l 32
In a defense-in-depth security approach, peppering can also be used. This is a security technique where an external secret value
is added to a password before it is hashed.
This makes it more difficult for an attacker to crack the hashed passwords, as they would need to know
the secret value to generate the correct hash.
Learn more here.
For password hashing:
import bcrypt
def hash_password(password):
return bcrypt.hashpw(password, bcrypt.gensalt(2)) # Noncompliant
For key derivation:
import bcrypt
def kdf(password, salt):
return bcrypt.kdf(
password=password,
salt=salt,
desired_key_bytes=32,
rounds=12, # Noncompliant
ignore_few_rounds=True)
For password hashing:
import bcrypt
def hash_password(password):
return bcrypt.hashpw(password, bcrypt.gensalt())
For key derivation:
import bcrypt
def kdf(password, salt):
return bcrypt.kdf(
password=password,
salt=salt,
desired_key_bytes=32,
rounds=4096)
In general, you should rely on an algorithm that has no known security vulnerabilities. The MD5 and SHA-1 algorithms should not be used.
Some algorithms, such as the SHA family functions, are considered strong for some use cases, but are too fast in computation and therefore vulnerable to brute force attacks, especially with bruteforce-attack-oriented hardware.
To protect passwords, it is therefore important to choose modern, slow password-hashing algorithms. The following algorithms are, in order of strength, the most secure password hashing algorithms to date:
Argon2 should be the best choice, and others should be used when the previous one is not available. For systems that must use FIPS-140-certified algorithms, PBKDF2 should be used.
Whenever possible, choose the strongest algorithm available. If the algorithm currently used by your system should be upgraded, OWASP documents possible upgrade methods here: Upgrading Legacy Hashes.
When bcrypt’s hashing function is used, it is important to select a round count that is high enough to make the function slow enough to prevent brute force: More than 12 rounds.
For bcrypt’s key derivation function, the number of rounds should likewise be high enough to make the function slow enough to prevent brute force:
More than 4096 rounds (2^12).
This number is not the same coefficient as the first one because it uses a different algorithm.
In the python bcrypt library, the default number of rounds is 12, which is a good default value.
For the bcrypt.kdf function, at
least 50 rounds should be set, and the ignore_few_rounds parameter should be avoided, as it allows fewer rounds.
As bcrypt has a maximum length input length of 72 bytes for most implementations, some developers may be tempted to pre-hash the password with a stronger algorithm before hashing it with bcrypt.
Pre-hashing passwords with bcrypt is not recommended as it can lead to a specific range of issues. Using a strong salt and a high number of rounds is enough to protect the password.
More information about this can be found here: Pre-hashing Passwords with Bcrypt.
In a defense-in-depth security approach, peppering can also be used. This is a security technique where an external secret value
is added to a password before it is hashed.
This makes it more difficult for an attacker to crack the hashed passwords, as they would need to know
the secret value to generate the correct hash.
Learn more here.
Code targeting scrypt:
from hashlib import scrypt
def hash_password(password, salt):
return scrypt(
password,
salt,
n=1 << 10, # Noncompliant: N is too low
r=8,
p=2,
dklen=64
)
Code targeting PBKDF2:
from hashlib import pbkdf2_hmac
def hash_password(password, salt):
return pbkdf2_hmac(
'sha1',
password,
salt,
500_000 # Noncompliant: not enough iterations for SHA-1
)
Code targeting scrypt:
from hashlib import scrypt
def hash_password(password, salt):
return scrypt(
password,
salt,
n=1 << 14,
r=8,
p=5,
dklen=64,
maxmem=85_000_000 # Needs ~85MB of memory
)
Code targeting PBKDF2:
from hashlib import pbkdf2_hmac
def hash_password(password, salt):
return pbkdf2_hmac(
'sha256',
password,
salt,
600_000
)
The following sections provide guidance on the usage of these secure password-hashing algorithms as provided by hashlib.
If scrypt must be used, the default values of scrypt are considered secure.
Like Argon2id, scrypt has three different parameters that can be configured. N is the CPU/memory cost parameter and must be a power of two. r is the block size and p is the parallelization factor.
All three parameters affect the memory and CPU usage of the algorithm. Higher values of N, r and p result in safer hashes, but come at the cost of higher resource usage.
For scrypt, OWASP recommends to have a hash length of at least 64 bytes, and to set N, p and r to the values of one of the following rows:
| N (cost parameter) | p (parallelization factor) | r (block size) |
|---|---|---|
| 217 ( |
1 |
8 |
| 216 ( |
2 |
8 |
| 215 ( |
3 |
8 |
| 214 ( |
5 |
8 |
| 213 ( |
10 |
8 |
Every row provides the same level of defense. They only differ in the amount of CPU and RAM used: the top row has low CPU usage and high memory usage, while the bottom row has high CPU usage and low memory usage.
If PBKDF2 must be used, be aware that default values might not be considered secure.
Depending on the algorithm used, the number of iterations
should be adjusted to ensure that the derived key is secure. The following are the recommended number of iterations for PBKDF2:
Note that PBKDF2-HMAC-SHA256 is recommended by NIST.
Iterations are also called "rounds" depending on the library used.
When recommended cost factors are too high in the context of the application or if the performance cost is unacceptable, a cost factor reduction might be considered. In that case, it should not be chosen under 100,000.
As bcrypt has a maximum length input length of 72 bytes for most implementations, some developers may be tempted to pre-hash the password with a stronger algorithm before hashing it with bcrypt.
Pre-hashing passwords with bcrypt is not recommended as it can lead to a specific range of issues. Using a strong salt and a high number of rounds is enough to protect the password.
More information about this can be found here: Pre-hashing Passwords with Bcrypt.
In a defense-in-depth security approach, peppering can also be used. This is a security technique where an external secret value
is added to a password before it is hashed.
This makes it more difficult for an attacker to crack the hashed passwords, as they would need to know
the secret value to generate the correct hash.
Learn more here.
Code targeting scrypt:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
def hash_password(password, salt):
scrypt = Scrypt(
salt=salt,
length=32,
n=1 << 10,
r=8,
p=1) # Noncompliant
return scrypt.derive(password)
Code targeting PBKDF2:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def hash_password(password, salt):
pbkdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=480000) # Noncompliant
return pbkdf.derive(password)
Code targeting scrypt:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
def hash_password(password, salt):
scrypt = Scrypt(
salt=salt,
length=64,
n=1 << 17,
r=8,
p=1)
return scrypt.derive(password)
Code targeting PBKDF2:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def hash_password(password, salt):
pbkdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=600_000) # Noncompliant
return pbkdf.derive(password)
In general, you should rely on an algorithm that has no known security vulnerabilities. The MD5 and SHA-1 algorithms should not be used.
Some algorithms, such as the SHA family functions, are considered strong for some use cases, but are too fast in computation and therefore vulnerable to brute force attacks, especially with bruteforce-attack-oriented hardware.
To protect passwords, it is therefore important to choose modern, slow password-hashing algorithms. The following algorithms are, in order of strength, the most secure password hashing algorithms to date:
Argon2 should be the best choice, and others should be used when the previous one is not available. For systems that must use FIPS-140-certified algorithms, PBKDF2 should be used.
Whenever possible, choose the strongest algorithm available. If the algorithm currently used by your system should be upgraded, OWASP documents possible upgrade methods here: Upgrading Legacy Hashes.
The following sections provide guidance on the usage of these secure password-hashing algorithms as provided by pyca/cryptography.
If scrypt must be used, the default values of scrypt are considered secure.
Like Argon2id, scrypt has three different parameters that can be configured. N is the CPU/memory cost parameter and must be a power of two. r is the block size and p is the parallelization factor.
All three parameters affect the memory and CPU usage of the algorithm. Higher values of N, r and p result in safer hashes, but come at the cost of higher resource usage.
For scrypt, OWASP recommends to have a hash length of at least 64 bytes, and to set N, p and r to the values of one of the following rows:
| N (cost parameter) | p (parallelization factor) | r (block size) |
|---|---|---|
| 217 ( |
1 |
8 |
| 216 ( |
2 |
8 |
| 215 ( |
3 |
8 |
| 214 ( |
5 |
8 |
| 213 ( |
10 |
8 |
Every row provides the same level of defense. They only differ in the amount of CPU and RAM used: the top row has low CPU usage and high memory usage, while the bottom row has high CPU usage and low memory usage.
To use values recommended by OWASP, you can use an object crafted as follows:
OWASP_1 = {
"n": 1 << 17,
"r": 8,
"p": 1,
"length": 64,
}
# To use this example, you can use the dictionary as a ``**kwargs`` variable:
scrypt(password, salt, **OWASP_1)
If PBKDF2 must be used, be aware that default values might not be considered secure.
Depending on the algorithm used, the number of iterations
should be adjusted to ensure that the derived key is secure. The following are the recommended number of iterations for PBKDF2:
Note that PBKDF2-HMAC-SHA256 is recommended by NIST.
Iterations are also called "rounds" depending on the library used.
When recommended cost factors are too high in the context of the application or if the performance cost is unacceptable, a cost factor reduction might be considered. In that case, it should not be chosen under 100,000.
As bcrypt has a maximum length input length of 72 bytes for most implementations, some developers may be tempted to pre-hash the password with a stronger algorithm before hashing it with bcrypt.
Pre-hashing passwords with bcrypt is not recommended as it can lead to a specific range of issues. Using a strong salt and a high number of rounds is enough to protect the password.
More information about this can be found here: Pre-hashing Passwords with Bcrypt.
In a defense-in-depth security approach, peppering can also be used. This is a security technique where an external secret value
is added to a password before it is hashed.
This makes it more difficult for an attacker to crack the hashed passwords, as they would need to know
the secret value to generate the correct hash.
Learn more here.
Django uses the first item in the PASSWORD_HASHERS list to store new passwords. In this example, SHA-1 is used, which is too fast to
store passwords.
# settings.py
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.SHA1PasswordHasher', # Noncompliant
'django.contrib.auth.hashers.CryptPasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.ScryptPasswordHasher',
]
This example requires argon2-cffi to be installed, which can be done using pip install django[argon2].
# settings.py
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.ScryptPasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
]
In general, you should rely on an algorithm that has no known security vulnerabilities. The MD5 and SHA-1 algorithms should not be used.
Some algorithms, such as the SHA family functions, are considered strong for some use cases, but are too fast in computation and therefore vulnerable to brute force attacks, especially with bruteforce-attack-oriented hardware.
To protect passwords, it is therefore important to choose modern, slow password-hashing algorithms. The following algorithms are, in order of strength, the most secure password hashing algorithms to date:
Argon2 should be the best choice, and others should be used when the previous one is not available. For systems that must use FIPS-140-certified algorithms, PBKDF2 should be used.
Whenever possible, choose the strongest algorithm available. If the algorithm currently used by your system should be upgraded, OWASP documents possible upgrade methods here: Upgrading Legacy Hashes.
In the previous example, Argon2 is used as the default password hashing function by Django. Use the PASSWORD_HASHERS variable
carefuly. If there is a need to upgrade, use Django’s
password upgrade documentation.
It is possible to change the parameters of the password hashing algorithm to make it more secure. For example, you can increase the number of
iterations or the length of the salt.
The Django documentation contains
more details about these parameters.
Django uses the first item in PASSWORD_HASHERS to store passwords, but uses every hashing algorithm in the
PASSWORD_HASHERS list to check passwords during user login. If a user password was not hashed using the first algorithm, then Django
upgrades the hashed password after a user logs in.
This process is convenient to keep users up to date, but is also vulnerable to enumeration. If an attacker wants to know whether an account exists, they can attempt a login with that account. By tracking how long it took to get a response, they can know if an older hashing algorithm was used (so the account exists) or the new hashing algorithm was used (the default is an account does not exist.)
To fix this, the Django documentation defines how to upgrade passwords without needing to log in. In this case, a custom hasher has to be created that wraps the old hash.
In a defense-in-depth security approach, peppering can also be used. This is a security technique where an external secret value
is added to a password before it is hashed.
This makes it more difficult for an attacker to crack the hashed passwords, as they would need to know
the secret value to generate the correct hash.
Learn more here.
from flask import Flask, request
from flask_bcrypt import Bcrypt
app = Flask(__name__)
bcrypt = Bcrypt(app)
@app.get("/")
def hash():
password = request.args.get('password', '')
hashed_password = bcrypt.generate_password_hash(password, rounds=2) # Noncompliant
return f"<p>{hashed_password.decode('utf-8')}</p>"
from flask import Flask, request
from flask_bcrypt import Bcrypt
app = Flask(__name__)
bcrypt = Bcrypt(app)
@app.get("/")
def hash():
password = request.args.get('password', '')
hashed_password = bcrypt.generate_password_hash(password)
return f"<p>{hashed_password.Decode('utf-8')}</p>"
In general, you should rely on an algorithm that has no known security vulnerabilities. The MD5 and SHA-1 algorithms should not be used.
Some algorithms, such as the SHA family functions, are considered strong for some use cases, but are too fast in computation and therefore vulnerable to brute force attacks, especially with bruteforce-attack-oriented hardware.
To protect passwords, it is therefore important to choose modern, slow password-hashing algorithms. The following algorithms are, in order of strength, the most secure password hashing algorithms to date:
Argon2 should be the best choice, and others should be used when the previous one is not available. For systems that must use FIPS-140-certified algorithms, PBKDF2 should be used.
Whenever possible, choose the strongest algorithm available. If the algorithm currently used by your system should be upgraded, OWASP documents possible upgrade methods here: Upgrading Legacy Hashes.
When bcrypt’s hashing function is used, it is important to select a round count that is high enough to make the function slow enough to prevent brute force: More than 12 rounds.
For bcrypt’s key derivation function, the number of rounds should likewise be high enough to make the function slow enough to prevent brute force:
More than 4096 rounds (2^12).
This number is not the same coefficient as the first one because it uses a different algorithm.
In general, the default values of the Argon2 library are considered safe. If you need to change the parameters, you should note the following:
First, Argon2 comes in three forms: Argon2i, Argon2d and Argon2id. Argon2i is optimized for hashing passwords and uses data-independent memory
access. Argon2d is faster and uses data-dependent memory access, making it suitable for applications where there is no risk of side-channel
attacks.
Argon2id is a mixture of Argon2i and Argon2d and is recommended for most applications.
Argon2id has three different parameters that can be configured: the basic minimum memory size (m), the minimum number of iterations (t) and the
degree of parallelism (p).
The higher the values of m, t and p result in safer hashes, but come at the cost of higher resource usage. There exist
general recommendations that balance security and speed in an optimal way.
Hashes should be at least 32 bytes long and salts should be at least 16 bytes long.
Next, the recommended parameters for Argon2id are:
| Recommendation type | Time Cost | Memory Cost | Parallelism |
|---|---|---|---|
| Argon2 Creators |
1 |
2097152 (2 GiB) |
4 |
| Argon2 Creators |
3 |
65536 (64 MiB) |
4 |
| OWASP |
1 |
47104 (46 MiB) |
1 |
| OWASP |
2 |
19456 (19 MiB) |
1 |
| OWASP |
3 |
12288 (12 MiB) |
1 |
| OWASP |
4 |
9216 (9 MiB) |
1 |
| OWASP |
5 |
7168 (7 MiB) |
1 |
To use values recommended by the Argon2 authors, you can use the two following objects:
To use values recommended by the OWASP, you can craft objects as follows:
import argon2
from argon2.low_level import ARGON2_VERSION, Type
OWASP_1 = argon2.Parameters(
type=Type.ID,
version=ARGON2_VERSION,
salt_len=16,
hash_len=32,
time_cost=1,
memory_cost=47104, # 46 MiB
parallelism=1)
# To apply the parameters to the Flask app:
def set_flask_argon2_parameters(app, parameters: argon2.Parameters):
app.config['ARGON2_SALT_LENGTH'] = parameters.salt_len
app.config['ARGON2_HASH_LENGTH'] = parameters.hash_len
app.config['ARGON2_TIME_COST'] = parameters.time_cost
app.config['ARGON2_MEMORY_COST'] = parameters.memory_cost
app.config['ARGON2_PARALLELISM'] = parameters.parallelism
# ----
# Or the unofficial way:
from flask import Flask
from flask_argon2 import Argon2
app = Flask(__name__)
argon2 = Argon2(app)
argon2.ph = OWASP_1
set_flask_argon2_parameters(app, OWASP_1)
As bcrypt has a maximum length input length of 72 bytes for most implementations, some developers may be tempted to pre-hash the password with a stronger algorithm before hashing it with bcrypt.
Pre-hashing passwords with bcrypt is not recommended as it can lead to a specific range of issues. Using a strong salt and a high number of rounds is enough to protect the password.
More information about this can be found here: Pre-hashing Passwords with Bcrypt.
To determine which one is the most appropriate for your application, you can use the argon2 CLI, for example with OWASP’s first recommendation:
$ pip install argon2 $ python -m argon2 -t 1 -m 47104 -p 1 -l 32
In a defense-in-depth security approach, peppering can also be used. This is a security technique where an external secret value
is added to a password before it is hashed.
This makes it more difficult for an attacker to crack the hashed passwords, as they would need to know
the secret value to generate the correct hash.
Learn more here.