Skip to content

Commit

Permalink
Add rule to check for improper random generator usage (#394)
Browse files Browse the repository at this point in the history
Specifically in the hashlib module, it specifies that secure
alternatives to the random module should be used for crypto functions.

This rule checks various hashlib functions where a salt is provided via
an insecure random function such as random.randbytes() or
ssl.RAND_bytes()

Closes #229

Signed-off-by: Eric Brown <eric.brown@securesauce.dev>
  • Loading branch information
ericwb committed Mar 27, 2024
1 parent 72f9156 commit 3dfb971
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@
| PY032 | [xmlrpc — unrestricted bind](rules/python/stdlib/xmlrpc-server-unrestricted-bind.md) | Binding to an Unrestricted IP Address in `xmlrpc.server` Module |
| PY033 | [re — denial of service](rules/python/stdlib/re-denial-of-service.md) | Inefficient Regular Expression Complexity in `re` Module |
| PY034 | [hmac — weak key](rules/python/stdlib/hmac-weak-key.md) | Insufficient `hmac` Key Size |
| PY035 | [hashlib — improper prng](rules/python/stdlib/hashlib-improper-prng.md) | Improper Randomness for Cryptographic `hashlib` Functions |
10 changes: 10 additions & 0 deletions docs/rules/python/stdlib/hashlib-improper-prng.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
id: PY035
title: hashlib — improper prng
hide_title: true
pagination_prev: null
pagination_next: null
slug: /rules/PY035
---

::: precli.rules.python.stdlib.hashlib_improper_prng
142 changes: 142 additions & 0 deletions precli/rules/python/stdlib/hashlib_improper_prng.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Copyright 2024 Secure Saurce LLC
r"""
# Improper Randomness for Cryptographic `hashlib` Functions
This rule detects the use of non-cryptographically secure randomness sources,
such as Python's `random()` function, as inputs to cryptographic functions
like `hashlib.scrypt()`. Using non-secure randomness sources can weaken the
cryptographic strength of functions that rely on unpredictability for security.
Cryptographic functions, including key generation, encryption, and hashing,
require a source of randomness that is unpredictable and secure against
attack. The standard `random()` function in Python is designed for statistical
modeling and simulations, not for security purposes, as it generates
predictable sequences that can be reproduced if the seed value is known.
Using `random()` for cryptographic purposes, such as generating salts or keys,
compromises security by making the output potentially predictable to attackers.
Ensure all cryptographic operations utilize a cryptographically secure source
of randomness. Python provides the `secrets` module for generating secure
random numbers suitable for security-sensitive applications, including key
generation and creating salts for hashing functions.
## Example
```python
import hashlib
import random
password = b"my_secure_password"
salt = random.randbytes(16)
hashlib.scrypt(password, salt=salt, n=16384, r=8, p=1)
```
## Remediation
For security or cryptographic uses use a secure pseudo-random generator such
as `os.urandom()` or `secrets.token_bytes()`.
```python
import hashlib
import os
password = b"my_secure_password"
salt = os.urandom(16)
hashlib.scrypt(password, salt=salt, n=16384, r=8, p=1)
```
## See also
- [random — Generate pseudo-random numbers](https://docs.python.org/3/library/random.html#random.randbytes)
- [hashlib — Secure hashes and message digests](https://docs.python.org/3/library/hashlib.html)
- [ssl — TLS_SSL wrapper for socket objects](https://docs.python.org/3/library/ssl.html#ssl.RAND_bytes)
- [CWE-330: Use of Insufficiently Random Values](https://cwe.mitre.org/data/definitions/330.html)
_New in version 0.4.3_
""" # noqa: E501
from precli.core.call import Call
from precli.core.location import Location
from precli.core.result import Result
from precli.rules import Rule


class HashlibImproperPrng(Rule):
def __init__(self, id: str):
super().__init__(
id=id,
name="improper_random",
description=__doc__,
cwe_id=330,
message="The '{0}' pseudo-random generator should not be used for "
"security purposes.",
wildcards={
"hashlib.*": [
"blake2b",
"blake2s",
"pbkdf2_hmac",
"scrypt",
],
"random.*": [
"randbytes",
],
"ssl.*": [
"RAND_bytes",
],
},
)

def analyze_call(self, context: dict, call: Call) -> Result:
if call.name_qualified not in (
"hashlib.blake2b",
"hashlib.blake2s",
"hashlib.pbkdf2_hmac",
"hashlib.scrypt",
):
return

"""
hashlib.blake2b(data=b'', *, digest_size=64, key=b'', salt=b'',
person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0,
node_depth=0, inner_size=0, last_node=False, usedforsecurity=True)
hashlib.blake2s(data=b'', *, digest_size=32, key=b'', salt=b'',
person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0,
node_depth=0, inner_size=0, last_node=False, usedforsecurity=True)
hashlib.pbkdf2_hmac(hash_name, password, salt, iterations, dklen=None)
hashlib.scrypt(password, *, salt, n, r, p, maxmem=0, dklen=64)
"""

if call.name_qualified == "hashlib.pbkdf2_hmac":
argument = call.get_argument(position=2, name="salt")
elif call.name_qualified in (
"hashlib.blake2b",
"hashlib.blake2s",
"hashlib.scrypt",
):
argument = call.get_argument(name="salt")

if argument.value not in (
"random.randbytes",
"ssl.RAND_bytes",
):
return

fixes = Rule.get_fixes(
context=context,
deleted_location=Location(node=argument.node),
description="The salt should be 16 or more bytes from a proper "
"pseudo-random source such as `os.urandom()`.",
inserted_content="os.urandom(16)",
)

return Result(
rule_id=self.id,
location=Location(node=argument.node),
message=self.message.format(argument.value),
fixes=fixes,
)
1 change: 1 addition & 0 deletions precli/rules/python/stdlib/hashlib_weak_hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def __init__(self, id: str):
"ripemd160",
"sha",
"sha1",
"pbkdf2_hmac",
]
},
config=Config(level=Level.ERROR),
Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,8 @@ precli.rules.python =
# precli/rules/python/stdlib/hmac_weak_key.py
PY034 = precli.rules.python.stdlib.hmac_weak_key:HmacWeakKey

# precli/rules/python/stdlib/hashlib_improper_prng.py
PY035 = precli.rules.python.stdlib.hashlib_improper_prng:HashlibImproperPrng

[build_sphinx]
all_files = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# level: WARNING
# start_line: 12
# end_line: 12
# start_column: 27
# end_column: 31
import hashlib
import ssl


data = b"super-secret-data"
salt = ssl.RAND_bytes(16)
hashlib.blake2b(data, salt=salt)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# level: WARNING
# start_line: 12
# end_line: 12
# start_column: 27
# end_column: 31
import hashlib
from random import randbytes


data = b"super-secret-data"
salt = randbytes(16)
hashlib.blake2s(data, salt=salt)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# level: WARNING
# start_line: 13
# end_line: 13
# start_column: 58
# end_column: 62
import hashlib
import ssl


password = b"my_secure_password"
salt = ssl.RAND_bytes(16)
our_app_iters = 500_000
hashed_password = hashlib.pbkdf2_hmac("sha256", password, salt, our_app_iters)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# level: WARNING
# start_line: 12
# end_line: 12
# start_column: 48
# end_column: 52
import hashlib
import random


password = b"my_secure_password"
salt = random.randbytes(16)
hashed_password = hashlib.scrypt(password, salt=salt, n=16384, r=8, p=1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2024 Secure Saurce LLC
import os

from parameterized import parameterized

from precli.core.level import Level
from precli.parsers import python
from precli.rules import Rule
from tests.unit.rules import test_case


class HashlibImproperPrngTests(test_case.TestCase):
def setUp(self):
super().setUp()
self.rule_id = "PY035"
self.parser = python.Python()
self.base_path = os.path.join(
"tests",
"unit",
"rules",
"python",
"stdlib",
"hashlib",
"examples",
)

def test_rule_meta(self):
rule = Rule.get_by_id(self.rule_id)
self.assertEqual(self.rule_id, rule.id)
self.assertEqual("improper_random", rule.name)
self.assertEqual(
f"https://docs.securesauce.dev/rules/{self.rule_id}", rule.help_url
)
self.assertEqual(True, rule.default_config.enabled)
self.assertEqual(Level.WARNING, rule.default_config.level)
self.assertEqual(-1.0, rule.default_config.rank)
self.assertEqual("330", rule.cwe.cwe_id)

@parameterized.expand(
[
"hashlib_improper_prng_blake2b.py",
"hashlib_improper_prng_blake2s.py",
"hashlib_improper_prng_pbkdf2_hmac.py",
"hashlib_improper_prng_scrypt.py",
]
)
def test(self, filename):
self.check(filename)

0 comments on commit 3dfb971

Please sign in to comment.