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
|
||||
* aiohttp
|
||||
* Amazon DynamoDB
|
||||
* Argon2-cffi
|
||||
* Bcrypt
|
||||
* Cryptodome
|
||||
* Django
|
||||
* Django Templates
|
||||
@ -98,6 +100,7 @@
|
||||
* Python Standard Library
|
||||
* PyYAML
|
||||
* Requests
|
||||
* Scrypt
|
||||
* SignXML
|
||||
* SQLAlchemy
|
||||
* ssl
|
||||
@ -132,4 +135,4 @@
|
||||
// Go
|
||||
* Go Standard Library
|
||||
// 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
|
||||
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].
|
||||
|
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[]
|
||||
|
||||
=== Pitfalls
|
||||
|
||||
include::../common/pitfalls/pre-hashing.adoc[]
|
||||
|
||||
|
||||
== Resources
|
||||
|
||||
=== 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