Create rule S5344(python): Passwords should not be stored in plain-text or with a fast hashing algorithm (#3715)

This commit is contained in:
github-actions[bot] 2024-03-18 17:37:51 +01:00 committed by GitHub
parent 576a6152e0
commit c5593190ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 758 additions and 9 deletions

View File

@ -76,6 +76,8 @@
// Python
* aiohttp
* Amazon DynamoDB
* Argon2-cffi
* Bcrypt
* Cryptodome
* Django
* Django Templates
@ -98,6 +100,7 @@
* Python Standard Library
* PyYAML
* Requests
* Scrypt
* SignXML
* SQLAlchemy
* ssl

View File

@ -0,0 +1,13 @@
==== 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:
[source,shell]
----
$ pip install argon2
$ python -m argon2 -t 1 -m 47104 -p 1 -l 32
----
https://argon2-cffi.readthedocs.io/en/stable/api.html#module-argon2.profiles[Learn more here].

View File

@ -0,0 +1,9 @@
==== 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. +
https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#peppering[Learn more here].

View File

@ -0,0 +1,55 @@
==== 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:
[options="header",cols="a,a,a,a"]
|===
|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
|===

View File

@ -0,0 +1,12 @@
==== 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.

View File

@ -1,11 +1,26 @@
==== Use specific password hashing algorithms
==== Use secure password hashing algorithms
In general, rely on an algorithm with no known weaknesses, and rule out the
others, such as MD5 or SHA-1.
In general, you should rely on an algorithm that has no known security
vulnerabilities. The MD5 and SHA-1 algorithms should not be used.
While considered strong for some use cases, some algorithms, like SHA-family
functions, are too fast to compute and therefore susceptible to brute force
attacks, especially with attack-dedicated hardware. +
Modern, slow, password-hashing algorithms such as *bcrypt*, *PBKDF2* or *argon2*
are recommended.
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
. scrypt
. bcrypt
. 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:
https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#upgrading-legacy-hashes[Upgrading Legacy Hashes].

View File

@ -0,0 +1,15 @@
==== 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:
* PBKDF2-HMAC-SHA1: 1,300,000 iterations
* PBKDF2-HMAC-SHA256: 600,000 iterations
* PBKDF2-HMAC-SHA512: 210,000 iterations
Note that PBKDF2-HMAC-SHA256 is recommended by NIST. +
Iterations are also called "rounds" depending on the library used.

View File

@ -0,0 +1,32 @@
==== 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:
[options="header",cols="a,a,a"]
|===
|N (cost parameter) |p (parallelization factor) |r (block size)
|2^17^ (`1 << 17`)
|1
|8
|2^16^ (`1 << 16`)
|2
|8
|2^15^ (`1 << 15`)
|3
|8
|2^14^ (`1 << 14`)
|5
|8
|2^13^ (`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.

View File

@ -0,0 +1,13 @@
==== 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:
https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pre-hashing-passwords-with-bcrypt[Pre-hashing Passwords with Bcrypt].

View File

@ -50,6 +50,11 @@ adaptative, and automatically implements a salt.
include::../common/fix/plaintext-password.adoc[]
=== Pitfalls
include::../common/pitfalls/pre-hashing.adoc[]
== Resources
=== Documentation

View File

@ -0,0 +1,9 @@
=== Highlighting
For hashlib:
* Primary highlight on the incorrect hashing parameter(s)
For Django:
* Primary highlight on the hashing algorithm that caused the issue

View File

@ -0,0 +1,62 @@
== How to fix it in Argon2-cffi
=== Code examples
==== Noncompliant code example
[source,python,diff-id=200,diff-type=noncompliant]
----
from argon2 import PasswordHasher, profiles
def hash_password(password):
ph = PasswordHasher.from_parameters(profiles.CHEAPEST) # Noncompliant
return ph.hash(password)
----
==== Compliant solution
[source,python,diff-id=200,diff-type=compliant]
----
from argon2 import PasswordHasher
def hash_password(password):
ph = PasswordHasher()
return ph.hash(password)
----
=== How does this work?
include::../../common/fix/argon-parameters.adoc[]
To use values recommended by the Argon2 authors, you can use the following objects:
* https://argon2-cffi.readthedocs.io/en/stable/api.html#argon2.profiles.RFC_9106_HIGH_MEMORY[argon2.profiles.RFC_9106_HIGH_MEMORY]
* https://argon2-cffi.readthedocs.io/en/stable/api.html#argon2.profiles.RFC_9106_LOW_MEMORY[argon2.profiles.RFC_9106_LOW_MEMORY]
To use values recommended by the OWASP you can craft objects as follows:
[source, python]
----
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
include::../../common/extra-mile/argon-cli.adoc[]
include::../../common/extra-mile/peppering.adoc[]

View File

@ -0,0 +1,73 @@
== How to fix it in Bcrypt
=== Code examples
==== Noncompliant code example
For password hashing:
[source,python,diff-id=201,diff-type=noncompliant]
----
import bcrypt
def hash_password(password):
return bcrypt.hashpw(password, bcrypt.gensalt(2)) # Noncompliant
----
For key derivation:
[source,python,diff-id=291,diff-type=noncompliant]
----
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:
[source,python,diff-id=201,diff-type=compliant]
----
import bcrypt
def hash_password(password):
return bcrypt.hashpw(password, bcrypt.gensalt())
----
For key derivation:
[source,python,diff-id=291,diff-type=compliant]
----
import bcrypt
def kdf(password, salt):
return bcrypt.kdf(
password=password,
salt=salt,
desired_key_bytes=32,
rounds=4096)
----
=== How does this work?
include::../../common/fix/password-hashing.adoc[]
include::../../common/fix/bcrypt-parameters.adoc[]
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
include::../../common/pitfalls/pre-hashing.adoc[]
=== Going the extra mile
include::../../common/extra-mile/peppering.adoc[]

View File

@ -0,0 +1,73 @@
== 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.
[source,python,diff-id=203,diff-type=noncompliant]
----
# 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]`.
[source,python,diff-id=203,diff-type=compliant]
----
# 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?
include::../../common/fix/password-hashing.adoc[]
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
https://docs.djangoproject.com/en/5.0/topics/auth/passwords/#password-upgrading[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. +
https://docs.djangoproject.com/en/5.0/topics/auth/passwords/[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, https://docs.djangoproject.com/en/5.0/topics/auth/passwords/#password-upgrading-without-requiring-a-login[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.
include::../../common/extra-mile/peppering.adoc[]

View File

@ -0,0 +1,99 @@
== How to fix it in Flask
=== Code examples
==== Noncompliant code example
[source,python,diff-id=204,diff-type=noncompliant]
----
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
[source,python,diff-id=204,diff-type=compliant]
----
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?
include::../../common/fix/password-hashing.adoc[]
include::../../common/fix/bcrypt-parameters.adoc[]
include::../../common/fix/argon-parameters.adoc[]
To use values recommended by the Argon2 authors, you can use the two following objects:
* https://argon2-cffi.readthedocs.io/en/stable/api.html#argon2.profiles.RFC_9106_HIGH_MEMORY[argon2.profiles.RFC_9106_HIGH_MEMORY]
* https://argon2-cffi.readthedocs.io/en/stable/api.html#argon2.profiles.RFC_9106_LOW_MEMORY[argon2.profiles.RFC_9106_LOW_MEMORY]
To use values recommended by the OWASP, you can craft objects as follows:
[source, python]
----
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
include::../../common/pitfalls/pre-hashing.adoc[]
=== Going the extra mile
include::../../common/extra-mile/argon-cli.adoc[]
include::../../common/extra-mile/peppering.adoc[]

View File

@ -0,0 +1,91 @@
== How to fix it in Python Standard Library
=== Code examples
==== Noncompliant code example
Code targeting scrypt:
[source,python,diff-id=206,diff-type=noncompliant]
----
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:
[source,python,diff-id=266,diff-type=noncompliant]
----
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:
[source,python,diff-id=206,diff-type=compliant]
----
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:
[source,python,diff-id=266,diff-type=compliant]
----
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.
include::../../common/fix/scrypt-parameters.adoc[]
include::../../common/fix/pbkdf2-parameters.adoc[]
=== Pitfalls
include::../../common/pitfalls/pre-hashing.adoc[]
=== Going the extra mile
include::../../common/extra-mile/peppering.adoc[]

View File

@ -0,0 +1,114 @@
== How to fix it in pyca
=== Code examples
==== Noncompliant code example
Code targeting scrypt:
[source,python,diff-id=207,diff-type=noncompliant]
----
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:
[source,python,diff-id=277,diff-type=noncompliant]
----
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:
[source,python,diff-id=207,diff-type=compliant]
----
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:
[source,python,diff-id=277,diff-type=compliant]
----
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?
include::../../common/fix/password-hashing.adoc[]
The following sections provide guidance on the usage of these secure
password-hashing algorithms as provided by pyca/cryptography.
include::../../common/fix/scrypt-parameters.adoc[]
To use values recommended by OWASP, you can use an object crafted as follows:
[source,python]
----
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)
----
include::../../common/fix/pbkdf2-parameters.adoc[]
=== Pitfalls
include::../../common/pitfalls/pre-hashing.adoc[]
=== Going the extra mile
include::../../common/extra-mile/peppering.adoc[]

View File

@ -0,0 +1,11 @@
=== Message
For hashlib:
* For scrypt: "Use strong scrypt parameters"
* For pbkdf2_hmac: "Use at least ``+{min_iterations}+`` PBKDF2 iterations"
** If `hash_name` is `"sha1"`, then min_iterations is 1300000
** If `hash_name` is `"sha256"`, then min_iterations is 600000
** If `hash_name` is `"sha512"`, then min_iterations is 210000
For Django: "Use a secure hashing algorithm to store passwords"

View File

@ -0,0 +1,2 @@
{
}

View File

@ -0,0 +1,43 @@
include::../summary.adoc[]
== Why is this an issue?
include::../rationale.adoc[]
include::../impact.adoc[]
// How to fix it section
include::how-to-fix-it/argon2.adoc[]
include::how-to-fix-it/bcrypt.adoc[]
include::how-to-fix-it/hashlib.adoc[]
include::how-to-fix-it/pyca.adoc[]
include::how-to-fix-it/django.adoc[]
include::how-to-fix-it/flask.adoc[]
== Resources
=== Documentation
* OWASP CheatSheet - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html[Password Storage Cheat Sheet]
include::../common/resources/standards.adoc[]
ifdef::env-github,rspecator-view[]
'''
== Implementation Specification
(visible only on this page)
include::message.adoc[]
include::highlighting.adoc[]
endif::env-github,rspecator-view[]