Skip to content

Commit c4da1cb

Browse files
authored
Adds check for crypt module usage as weak hash (#1018)
The crypt module also permits creating weak hashes such as MD5 just like hashlib. This change extends the hashlib plugin to add the extra checks on calls to crypt.crypt and crypt.mksalt which both take a hash method parameter. The new checks won't necessarily catch all weak hashes available as the operating system might provide others that the crypt module picks up. But it will capture cases with the default set. Namely, METHOD_CRYPT", METHOD_MD5, and METHOD_BLOWFISH. Also note that crypt.methods is supposed to return a list all available hash methods. However, testing has shown that it can return just METHOD_CRYPT, the weakest of the bunch. And passing None as args to these module functions will default to the highest available hash from crypt.methods. So this also can be weak by default, but no reliable for Bandit to detect. https://docs.python.org/3.11/library/crypt.html#module-crypt Fixes #1017 Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com>
1 parent 36fc7be commit c4da1cb

File tree

3 files changed

+104
-56
lines changed

3 files changed

+104
-56
lines changed

bandit/plugins/hashlib_insecure_functions.py

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
======================================================================
77
88
This plugin checks for the usage of the insecure MD4, MD5, or SHA1 hash
9-
functions in ``hashlib``. The ``hashlib.new`` function provides
9+
functions in ``hashlib`` and ``crypt``. The ``hashlib.new`` function provides
1010
the ability to construct a new hashing object using the named algorithm. This
1111
can be used to create insecure hash functions like MD4 and MD5 if they are
1212
passed as algorithm names to this function.
@@ -17,6 +17,10 @@
1717
does additional checking for usage of keyword usedforsecurity on all
1818
function variations of hashlib.
1919
20+
Similar to ``hashlib``, this plugin also checks for usage of one of the
21+
``crypt`` module's weak hashes. ``crypt`` also permits MD5 among other weak
22+
hash variants.
23+
2024
:Example:
2125
2226
.. code-block:: none
@@ -40,6 +44,9 @@
4044
.. versionchanged:: 1.7.3
4145
CWE information added
4246
47+
.. versionchanged:: 1.7.6
48+
Added check for the crypt module weak hashes
49+
4350
""" # noqa: E501
4451
import sys
4552

@@ -48,64 +55,90 @@
4855
from bandit.core import test_properties as test
4956

5057
WEAK_HASHES = ("md4", "md5", "sha", "sha1")
51-
52-
53-
def _hashlib_func(context):
54-
if isinstance(context.call_function_name_qual, str):
55-
qualname_list = context.call_function_name_qual.split(".")
56-
57-
if "hashlib" in qualname_list:
58-
func = qualname_list[-1]
59-
keywords = context.call_keywords
60-
61-
if func in WEAK_HASHES:
62-
if keywords.get("usedforsecurity", "True") == "True":
63-
return bandit.Issue(
64-
severity=bandit.HIGH,
65-
confidence=bandit.HIGH,
66-
cwe=issue.Cwe.BROKEN_CRYPTO,
67-
text=f"Use of weak {func.upper()} hash for security. "
68-
"Consider usedforsecurity=False",
69-
lineno=context.node.lineno,
70-
)
71-
elif func == "new":
72-
args = context.call_args
73-
name = args[0] if args else keywords.get("name", None)
74-
if isinstance(name, str) and name.lower() in WEAK_HASHES:
75-
if keywords.get("usedforsecurity", "True") == "True":
76-
return bandit.Issue(
77-
severity=bandit.HIGH,
78-
confidence=bandit.HIGH,
79-
cwe=issue.Cwe.BROKEN_CRYPTO,
80-
text=f"Use of weak {name.upper()} hash for "
81-
"security. Consider usedforsecurity=False",
82-
lineno=context.node.lineno,
83-
)
84-
85-
86-
def _hashlib_new(context):
87-
if isinstance(context.call_function_name_qual, str):
88-
qualname_list = context.call_function_name_qual.split(".")
89-
func = qualname_list[-1]
90-
91-
if "hashlib" in qualname_list and func == "new":
92-
args = context.call_args
93-
keywords = context.call_keywords
94-
name = args[0] if args else keywords.get("name", None)
95-
if isinstance(name, str) and name.lower() in WEAK_HASHES:
58+
WEAK_CRYPT_HASHES = ("METHOD_CRYPT", "METHOD_MD5", "METHOD_BLOWFISH")
59+
60+
61+
def _hashlib_func(context, func):
62+
keywords = context.call_keywords
63+
64+
if func in WEAK_HASHES:
65+
if keywords.get("usedforsecurity", "True") == "True":
66+
return bandit.Issue(
67+
severity=bandit.HIGH,
68+
confidence=bandit.HIGH,
69+
cwe=issue.Cwe.BROKEN_CRYPTO,
70+
text=f"Use of weak {func.upper()} hash for security. "
71+
"Consider usedforsecurity=False",
72+
lineno=context.node.lineno,
73+
)
74+
elif func == "new":
75+
args = context.call_args
76+
name = args[0] if args else keywords.get("name", None)
77+
if isinstance(name, str) and name.lower() in WEAK_HASHES:
78+
if keywords.get("usedforsecurity", "True") == "True":
9679
return bandit.Issue(
97-
severity=bandit.MEDIUM,
80+
severity=bandit.HIGH,
9881
confidence=bandit.HIGH,
9982
cwe=issue.Cwe.BROKEN_CRYPTO,
100-
text=f"Use of insecure {name.upper()} hash function.",
83+
text=f"Use of weak {name.upper()} hash for "
84+
"security. Consider usedforsecurity=False",
10185
lineno=context.node.lineno,
10286
)
10387

10488

89+
def _hashlib_new(context, func):
90+
if func == "new":
91+
args = context.call_args
92+
keywords = context.call_keywords
93+
name = args[0] if args else keywords.get("name", None)
94+
if isinstance(name, str) and name.lower() in WEAK_HASHES:
95+
return bandit.Issue(
96+
severity=bandit.MEDIUM,
97+
confidence=bandit.HIGH,
98+
cwe=issue.Cwe.BROKEN_CRYPTO,
99+
text=f"Use of insecure {name.upper()} hash function.",
100+
lineno=context.node.lineno,
101+
)
102+
103+
104+
def _crypt_crypt(context, func):
105+
args = context.call_args
106+
keywords = context.call_keywords
107+
108+
if func == "crypt":
109+
name = args[1] if len(args) > 1 else keywords.get("salt", None)
110+
if isinstance(name, str) and name in WEAK_CRYPT_HASHES:
111+
return bandit.Issue(
112+
severity=bandit.MEDIUM,
113+
confidence=bandit.HIGH,
114+
cwe=issue.Cwe.BROKEN_CRYPTO,
115+
text=f"Use of insecure crypt.{name.upper()} hash function.",
116+
lineno=context.node.lineno,
117+
)
118+
elif func == "mksalt":
119+
name = args[0] if args else keywords.get("method", None)
120+
if isinstance(name, str) and name in WEAK_CRYPT_HASHES:
121+
return bandit.Issue(
122+
severity=bandit.MEDIUM,
123+
confidence=bandit.HIGH,
124+
cwe=issue.Cwe.BROKEN_CRYPTO,
125+
text=f"Use of insecure crypt.{name.upper()} hash function.",
126+
lineno=context.node.lineno,
127+
)
128+
129+
105130
@test.test_id("B324")
106131
@test.checks("Call")
107132
def hashlib(context):
108-
if sys.version_info >= (3, 9):
109-
return _hashlib_func(context)
110-
else:
111-
return _hashlib_new(context)
133+
if isinstance(context.call_function_name_qual, str):
134+
qualname_list = context.call_function_name_qual.split(".")
135+
func = qualname_list[-1]
136+
137+
if "hashlib" in qualname_list:
138+
if sys.version_info >= (3, 9):
139+
return _hashlib_func(context, func)
140+
else:
141+
return _hashlib_new(context, func)
142+
143+
elif "crypt" in qualname_list and func in ("crypt", "mksalt"):
144+
return _crypt_crypt(context, func)

examples/crypto-md5.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from Cryptodome.Hash import MD5 as pycryptodomex_md5
99
from Cryptodome.Hash import SHA as pycryptodomex_sha
1010
import hashlib
11+
import crypt
1112

1213
hashlib.md5(1)
1314
hashlib.md5(1).hexdigest()
@@ -32,3 +33,17 @@
3233

3334
hashes.MD5()
3435
hashes.SHA1()
36+
37+
crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_CRYPT)
38+
crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_MD5)
39+
crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_BLOWFISH)
40+
crypt.crypt("asdfasdfasdfasdf")
41+
crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_SHA256)
42+
crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_SHA512)
43+
44+
crypt.mksalt(crypt.METHOD_CRYPT)
45+
crypt.mksalt(crypt.METHOD_MD5)
46+
crypt.mksalt(crypt.METHOD_BLOWFISH)
47+
crypt.mksalt()
48+
crypt.mksalt(crypt.METHOD_SHA256)
49+
crypt.mksalt(crypt.METHOD_SHA512)

tests/functional/test_functional.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,29 +113,29 @@ def test_crypto_md5(self):
113113
"SEVERITY": {
114114
"UNDEFINED": 0,
115115
"LOW": 0,
116-
"MEDIUM": 10,
116+
"MEDIUM": 16,
117117
"HIGH": 9,
118118
},
119119
"CONFIDENCE": {
120120
"UNDEFINED": 0,
121121
"LOW": 0,
122122
"MEDIUM": 0,
123-
"HIGH": 19,
123+
"HIGH": 25,
124124
},
125125
}
126126
else:
127127
expect = {
128128
"SEVERITY": {
129129
"UNDEFINED": 0,
130130
"LOW": 0,
131-
"MEDIUM": 16,
131+
"MEDIUM": 22,
132132
"HIGH": 4,
133133
},
134134
"CONFIDENCE": {
135135
"UNDEFINED": 0,
136136
"LOW": 0,
137137
"MEDIUM": 0,
138-
"HIGH": 20,
138+
"HIGH": 26,
139139
},
140140
}
141141
self.check_example("crypto-md5.py", expect)

0 commit comments

Comments
 (0)