Repository: PyCQA/bandit (python)

Issue:
Lower the severity of insecure hash functions (B303) for hashlib if usedforsecurity=False
hashlib supports setting the usedforsecurity=False when hashing or creating a hash function:
```
hashlib.md5(content, usedforsecurity=False)
hashlib.new('md5', usedforsecurity=False)
```

I suggest bandit could lower the severity, if usedforsecurity=False is present, since this is quite explicit.

But I would advise against ignoring such cases completely, since it says in the docs ( https://docs.python.org/3/library/hashlib.html#hash-algorithms ), that setting it to False enables the use of insecure algorithms in restricted environments, and someone may decide to use this flag as a workaround to use insecure hash functions.

Pull request description:
Check value of usedforsecurity for hashlib
In Python 3.9+ hashlib has a new argument named usedforsecurity
to indicate whether the hash is intended to be used for security
or not. The default value is True. So a user must explicit set
to False to state their non-security use.

As a result of this chnage in Python, the severity has been
moved up to HIGH if the usedforsecurity is True. But on earlier
versions of Python, the severity will remain at MEDIUM since
we don't know the intent of usage.

https://docs.python.org/3/library/hashlib.html#hashlib.new

Closes #748

Signed-off-by: Eric Brown <browne@vmware.com>

Patch:
diff --git a/bandit/plugins/hashlib_new_insecure_functions.py b/bandit/plugins/hashlib_new_insecure_functions.py
index 36e54b4..302a028 100644
--- a/bandit/plugins/hashlib_new_insecure_functions.py
+++ b/bandit/plugins/hashlib_new_insecure_functions.py
@@ -34,6 +34,8 @@ hash functions created using ``hashlib.new`` function.
     CWE information added
 
 """
+import sys
+
 import bandit
 from bandit.core import issue
 from bandit.core import test_properties as test
@@ -55,10 +57,29 @@ def hashlib_new(context):
                 "sha",
                 "sha1",
             ):
-                return bandit.Issue(
-                    severity=bandit.MEDIUM,
-                    confidence=bandit.HIGH,
-                    cwe=issue.Cwe.BROKEN_CRYPTO,
-                    text="Use of insecure MD4 or MD5 hash function.",
-                    lineno=context.node.lineno,
-                )
+                if sys.version_info >= (3, 9):
+                    # Python 3.9 includes a usedforsecurity argument
+                    usedforsecurity = (
+                        args[2]
+                        if len(args) > 2
+                        else keywords.get("usedforsecurity", "True")
+                    )
+
+                    if usedforsecurity == "True":
+                        return bandit.Issue(
+                            severity=bandit.HIGH,
+                            confidence=bandit.HIGH,
+                            cwe=issue.Cwe.BROKEN_CRYPTO,
+                            text="Use of insecure MD2, MD4, MD5, or SHA1 hash "
+                            "function.",
+                            lineno=context.node.lineno,
+                        )
+                else:
+                    return bandit.Issue(
+                        severity=bandit.MEDIUM,
+                        confidence=bandit.HIGH,
+                        cwe=issue.Cwe.BROKEN_CRYPTO,
+                        text="Use of insecure MD2, MD4, MD5, or SHA1 hash "
+                        "function.",
+                        lineno=context.node.lineno,
+                    )
diff --git a/examples/hashlib_new_insecure_functions.py b/examples/hashlib_new_insecure_functions.py
index 229d3f9..96a774a 100644
--- a/examples/hashlib_new_insecure_functions.py
+++ b/examples/hashlib_new_insecure_functions.py
@@ -10,6 +10,9 @@ hashlib.new('MD4', string='test')
 
 hashlib.new(string='test', name='MD5')
 
+# 3rd arg only availabe in Python 3.9+
+hashlib.new('md5', b'test', True)
+
 hashlib.new('sha1')
 
 hashlib.new(string='test', name='SHA1')
@@ -18,7 +21,16 @@ hashlib.new('sha', string='test')
 
 hashlib.new(name='SHA', string='test')
 
+# usedforsecurity arg only availabe in Python 3.9+
+hashlib.new('sha1', usedforsecurity=True)
+
 # Test that plugin does not flag valid hash functions.
 hashlib.new('sha256')
 
 hashlib.new('SHA512')
+
+# 3rd arg only availabe in Python 3.9+
+hashlib.new('md5', b'test', False)
+
+# usedforsecurity arg only availabe in Python 3.9+
+hashlib.new(name='sha1', usedforsecurity=False)


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index 897e8a6..c0a8030 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -800,10 +800,36 @@ class FunctionalTests(testtools.TestCase):
 
     def test_hashlib_new_insecure_functions(self):
         """Test insecure hash functions created by `hashlib.new`."""
-        expect = {
-            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 9, "HIGH": 0},
-            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 9},
-        }
+        if sys.version_info >= (3, 9):
+            expect = {
+                "SEVERITY": {
+                    "UNDEFINED": 0,
+                    "LOW": 0,
+                    "MEDIUM": 0,
+                    "HIGH": 11,
+                },
+                "CONFIDENCE": {
+                    "UNDEFINED": 0,
+                    "LOW": 0,
+                    "MEDIUM": 0,
+                    "HIGH": 11,
+                },
+            }
+        else:
+            expect = {
+                "SEVERITY": {
+                    "UNDEFINED": 0,
+                    "LOW": 0,
+                    "MEDIUM": 13,
+                    "HIGH": 0,
+                },
+                "CONFIDENCE": {
+                    "UNDEFINED": 0,
+                    "LOW": 0,
+                    "MEDIUM": 0,
+                    "HIGH": 13,
+                },
+            }
         self.check_example("hashlib_new_insecure_functions.py", expect)
 
     def test_blacklist_pycrypto(self):