Create rule S5344(python): Passwords should not be stored in plain-text or with a fast hashing algorithm (#3715)
This commit is contained in:
parent
576a6152e0
commit
c5593190ce
@ -76,6 +76,8 @@
|
|||||||
// Python
|
// Python
|
||||||
* aiohttp
|
* aiohttp
|
||||||
* Amazon DynamoDB
|
* Amazon DynamoDB
|
||||||
|
* Argon2-cffi
|
||||||
|
* Bcrypt
|
||||||
* Cryptodome
|
* Cryptodome
|
||||||
* Django
|
* Django
|
||||||
* Django Templates
|
* Django Templates
|
||||||
@ -98,6 +100,7 @@
|
|||||||
* Python Standard Library
|
* Python Standard Library
|
||||||
* PyYAML
|
* PyYAML
|
||||||
* Requests
|
* Requests
|
||||||
|
* Scrypt
|
||||||
* SignXML
|
* SignXML
|
||||||
* SQLAlchemy
|
* SQLAlchemy
|
||||||
* ssl
|
* ssl
|
||||||
@ -132,4 +135,4 @@
|
|||||||
// Go
|
// Go
|
||||||
* Go Standard Library
|
* Go Standard Library
|
||||||
// Kubernetes
|
// Kubernetes
|
||||||
* Helm
|
* Helm
|
||||||
|
13
rules/S5344/common/extra-mile/argon-cli.adoc
Normal file
13
rules/S5344/common/extra-mile/argon-cli.adoc
Normal 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].
|
||||||
|
|
9
rules/S5344/common/extra-mile/peppering.adoc
Normal file
9
rules/S5344/common/extra-mile/peppering.adoc
Normal 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].
|
||||||
|
|
55
rules/S5344/common/fix/argon-parameters.adoc
Normal file
55
rules/S5344/common/fix/argon-parameters.adoc
Normal 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
|
||||||
|
|===
|
||||||
|
|
12
rules/S5344/common/fix/bcrypt-parameters.adoc
Normal file
12
rules/S5344/common/fix/bcrypt-parameters.adoc
Normal 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.
|
||||||
|
|
@ -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
|
In general, you should rely on an algorithm that has no known security
|
||||||
others, such as MD5 or SHA-1.
|
vulnerabilities. The MD5 and SHA-1 algorithms should not be used.
|
||||||
|
|
||||||
While considered strong for some use cases, some algorithms, like SHA-family
|
Some algorithms, such as the SHA family functions, are considered strong for
|
||||||
functions, are too fast to compute and therefore susceptible to brute force
|
some use cases, but are too fast in computation and therefore vulnerable to
|
||||||
attacks, especially with attack-dedicated hardware. +
|
brute force attacks, especially with bruteforce-attack-oriented hardware.
|
||||||
Modern, slow, password-hashing algorithms such as *bcrypt*, *PBKDF2* or *argon2*
|
|
||||||
are recommended.
|
|
||||||
|
|
||||||
|
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].
|
||||||
|
15
rules/S5344/common/fix/pbkdf2-parameters.adoc
Normal file
15
rules/S5344/common/fix/pbkdf2-parameters.adoc
Normal 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.
|
||||||
|
|
32
rules/S5344/common/fix/scrypt-parameters.adoc
Normal file
32
rules/S5344/common/fix/scrypt-parameters.adoc
Normal 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.
|
13
rules/S5344/common/pitfalls/pre-hashing.adoc
Normal file
13
rules/S5344/common/pitfalls/pre-hashing.adoc
Normal 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].
|
||||||
|
|
@ -50,6 +50,11 @@ adaptative, and automatically implements a salt.
|
|||||||
|
|
||||||
include::../common/fix/plaintext-password.adoc[]
|
include::../common/fix/plaintext-password.adoc[]
|
||||||
|
|
||||||
|
=== Pitfalls
|
||||||
|
|
||||||
|
include::../common/pitfalls/pre-hashing.adoc[]
|
||||||
|
|
||||||
|
|
||||||
== Resources
|
== Resources
|
||||||
|
|
||||||
=== Documentation
|
=== Documentation
|
||||||
|
9
rules/S5344/python/highlighting.adoc
Normal file
9
rules/S5344/python/highlighting.adoc
Normal 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
|
62
rules/S5344/python/how-to-fix-it/argon2.adoc
Normal file
62
rules/S5344/python/how-to-fix-it/argon2.adoc
Normal 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[]
|
||||||
|
|
73
rules/S5344/python/how-to-fix-it/bcrypt.adoc
Normal file
73
rules/S5344/python/how-to-fix-it/bcrypt.adoc
Normal 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[]
|
||||||
|
|
73
rules/S5344/python/how-to-fix-it/django.adoc
Normal file
73
rules/S5344/python/how-to-fix-it/django.adoc
Normal 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[]
|
||||||
|
|
99
rules/S5344/python/how-to-fix-it/flask.adoc
Normal file
99
rules/S5344/python/how-to-fix-it/flask.adoc
Normal 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[]
|
||||||
|
|
91
rules/S5344/python/how-to-fix-it/hashlib.adoc
Normal file
91
rules/S5344/python/how-to-fix-it/hashlib.adoc
Normal 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[]
|
||||||
|
|
114
rules/S5344/python/how-to-fix-it/pyca.adoc
Normal file
114
rules/S5344/python/how-to-fix-it/pyca.adoc
Normal 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[]
|
||||||
|
|
11
rules/S5344/python/message.adoc
Normal file
11
rules/S5344/python/message.adoc
Normal 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"
|
2
rules/S5344/python/metadata.json
Normal file
2
rules/S5344/python/metadata.json
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
43
rules/S5344/python/rule.adoc
Normal file
43
rules/S5344/python/rule.adoc
Normal 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[]
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user