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.

Why is this an issue?

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.

What is the potential impact?

Plaintext or weakly hashed password storage poses a significant security risk to software applications.

Unauthorized Access

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.

Credential Reuse

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.

Regulatory Compliance

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.

How to fix it in Argon2-cffi

Code examples

Noncompliant code example

from argon2 import PasswordHasher, profiles

def hash_password(password):
    ph = PasswordHasher.from_parameters(profiles.CHEAPEST) # Noncompliant
    return ph.hash(password)

Compliant solution

from argon2 import PasswordHasher

def hash_password(password):
    ph = PasswordHasher()
    return ph.hash(password)

How does this work?

Select the correct Argon2 parameters

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)

Going the extra mile

Selecting safe custom parameters for Argon2

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

Learn more here.

Pepper

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.

How to fix it in Bcrypt

Code examples

Noncompliant code example

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)

Compliant solution

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)

How does this work?

Use secure password hashing algorithms

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:

  1. Argon2
  2. scrypt
  3. bcrypt
  4. PBKDF2

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.

Select the correct Bcrypt parameters

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.

Pitfalls

Pre-hashing passwords

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.

Going the extra mile

Pepper

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.

How to fix it in Python Standard Library

Code examples

Noncompliant code example

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
    )

Compliant solution

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
    )

How does this work?

The following sections provide guidance on the usage of these secure password-hashing algorithms as provided by hashlib.

Select the correct Scrypt parameters

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 << 17)

1

8

216 (1 << 16)

2

8

215 (1 << 15)

3

8

214 (1 << 14)

5

8

213 (1 << 13)

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.

Select the correct PBKDF2 parameters

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.

Pitfalls

Pre-hashing passwords

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.

Going the extra mile

Pepper

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.

How to fix it in pyca

Code examples

Noncompliant code example

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)

Compliant solution

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)

How does this work?

Use secure password hashing algorithms

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:

  1. Argon2
  2. scrypt
  3. bcrypt
  4. PBKDF2

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.

Select the correct Scrypt parameters

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 << 17)

1

8

216 (1 << 16)

2

8

215 (1 << 15)

3

8

214 (1 << 14)

5

8

213 (1 << 13)

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)

Select the correct PBKDF2 parameters

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.

Pitfalls

Pre-hashing passwords

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.

Going the extra mile

Pepper

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.

How to fix it in Django

Code examples

Noncompliant code example

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',
]

Compliant solution

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',
]

How does this work?

Use secure password hashing algorithms

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:

  1. Argon2
  2. scrypt
  3. bcrypt
  4. PBKDF2

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.

Going the extra mile

Tweaking password hashing parameters

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.

Preventing user enumeration attacks

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.

Pepper

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.

How to fix it in Flask

Code examples

Noncompliant code example

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>"

Compliant solution

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>"

How does this work?

Use secure password hashing algorithms

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:

  1. Argon2
  2. scrypt
  3. bcrypt
  4. PBKDF2

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.

Select the correct Bcrypt parameters

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.

Select the correct Argon2 parameters

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)

Pitfalls

Pre-hashing passwords

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.

Going the extra mile

Selecting safe custom parameters for Argon2

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

Learn more here.

Pepper

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.

Resources

Documentation

Standards