Repository: PyCQA/bandit
Pull request #1060 (2023-09-19 22:29:06)

feat(plugins): add support for `httpx` in `B113`
Resolves https://github.com/PyCQA/bandit/issues/1059.


Patch:
diff --git a/bandit/plugins/crypto_request_no_cert_validation.py b/bandit/plugins/crypto_request_no_cert_validation.py
index 223d421..11791ed 100644
--- a/bandit/plugins/crypto_request_no_cert_validation.py
+++ b/bandit/plugins/crypto_request_no_cert_validation.py
@@ -54,8 +54,8 @@ from bandit.core import test_properties as test
 @test.checks("Call")
 @test.test_id("B501")
 def request_with_no_cert_validation(context):
-    HTTP_VERBS = ("get", "options", "head", "post", "put", "patch", "delete")
-    HTTPX_ATTRS = ("request", "stream", "Client", "AsyncClient") + HTTP_VERBS
+    HTTP_VERBS = {"get", "options", "head", "post", "put", "patch", "delete"}
+    HTTPX_ATTRS = {"request", "stream", "Client", "AsyncClient"} | HTTP_VERBS
     qualname = context.call_function_name_qual.split(".")[0]
 
     if (
diff --git a/bandit/plugins/request_without_timeout.py b/bandit/plugins/request_without_timeout.py
index a418b6c..d571a49 100644
--- a/bandit/plugins/request_without_timeout.py
+++ b/bandit/plugins/request_without_timeout.py
@@ -4,7 +4,8 @@ r"""
 B113: Test for missing requests timeout
 =======================================
 
-This plugin test checks for ``requests`` calls without a timeout specified.
+This plugin test checks for ``requests`` or ``httpx`` calls without a timeout
+specified.
 
 Nearly all production code should use this parameter in nearly all requests,
 Failure to do so can cause your program to hang indefinitely.
@@ -17,7 +18,7 @@ Bandit will return a MEDIUM severity error.
 
 .. code-block:: none
 
-    >> Issue: [B113:request_without_timeout] Requests call without timeout
+    >> Issue: [B113:request_without_timeout] Call to requests without timeout
        Severity: Medium   Confidence: Low
        CWE: CWE-400 (https://cwe.mitre.org/data/definitions/400.html)
        More Info: https://bandit.readthedocs.io/en/latest/plugins/b113_request_without_timeout.html
@@ -27,7 +28,7 @@ Bandit will return a MEDIUM severity error.
     4	requests.get('https://gmail.com', timeout=None)
 
     --------------------------------------------------
-    >> Issue: [B113:request_without_timeout] Requests call with timeout set to None
+    >> Issue: [B113:request_without_timeout] Call to requests with timeout set to None
        Severity: Medium   Confidence: Low
        CWE: CWE-400 (https://cwe.mitre.org/data/definitions/400.html)
        More Info: https://bandit.readthedocs.io/en/latest/plugins/b113_request_without_timeout.html
@@ -42,6 +43,9 @@ Bandit will return a MEDIUM severity error.
 
 .. versionadded:: 1.7.5
 
+.. versionchanged:: 1.7.10
+    Added check for httpx module
+
 """  # noqa: E501
 import bandit
 from bandit.core import issue
@@ -51,17 +55,23 @@ from bandit.core import test_properties as test
 @test.checks("Call")
 @test.test_id("B113")
 def request_without_timeout(context):
-    http_verbs = ("get", "options", "head", "post", "put", "patch", "delete")
+    HTTP_VERBS = {"get", "options", "head", "post", "put", "patch", "delete"}
+    HTTPX_ATTRS = {"request", "stream", "Client", "AsyncClient"} | HTTP_VERBS
     qualname = context.call_function_name_qual.split(".")[0]
 
-    if qualname == "requests" and context.call_function_name in http_verbs:
+    if (
+        qualname == "requests"
+        and context.call_function_name in HTTP_VERBS
+        or qualname == "httpx"
+        and context.call_function_name in HTTPX_ATTRS
+    ):
         # check for missing timeout
         if context.check_call_arg_value("timeout") is None:
             return bandit.Issue(
                 severity=bandit.MEDIUM,
                 confidence=bandit.LOW,
                 cwe=issue.Cwe.UNCONTROLLED_RESOURCE_CONSUMPTION,
-                text="Requests call without timeout",
+                text=f"Call to {qualname} without timeout",
             )
         # check for timeout=None
         if context.check_call_arg_value("timeout", "None"):
@@ -69,5 +79,5 @@ def request_without_timeout(context):
                 severity=bandit.MEDIUM,
                 confidence=bandit.LOW,
                 cwe=issue.Cwe.UNCONTROLLED_RESOURCE_CONSUMPTION,
-                text="Requests call with timeout set to None",
+                text=f"Call to {qualname} with timeout set to None",
             )
diff --git a/examples/requests-missing-timeout.py b/examples/requests-missing-timeout.py
index 38f2444..fa71c4b 100644
--- a/examples/requests-missing-timeout.py
+++ b/examples/requests-missing-timeout.py
@@ -1,27 +1,68 @@
+import httpx
 import requests
 import not_requests
 
+# Errors
 requests.get('https://gmail.com')
 requests.get('https://gmail.com', timeout=None)
-requests.get('https://gmail.com', timeout=5)
 requests.post('https://gmail.com')
 requests.post('https://gmail.com', timeout=None)
-requests.post('https://gmail.com', timeout=5)
 requests.put('https://gmail.com')
 requests.put('https://gmail.com', timeout=None)
-requests.put('https://gmail.com', timeout=5)
 requests.delete('https://gmail.com')
 requests.delete('https://gmail.com', timeout=None)
-requests.delete('https://gmail.com', timeout=5)
 requests.patch('https://gmail.com')
 requests.patch('https://gmail.com', timeout=None)
-requests.patch('https://gmail.com', timeout=5)
 requests.options('https://gmail.com')
 requests.options('https://gmail.com', timeout=None)
-requests.options('https://gmail.com', timeout=5)
 requests.head('https://gmail.com')
 requests.head('https://gmail.com', timeout=None)
-requests.head('https://gmail.com', timeout=5)
+httpx.get('https://gmail.com')
+httpx.get('https://gmail.com', timeout=None)
+httpx.post('https://gmail.com')
+httpx.post('https://gmail.com', timeout=None)
+httpx.put('https://gmail.com')
+httpx.put('https://gmail.com', timeout=None)
+httpx.delete('https://gmail.com')
+httpx.delete('https://gmail.com', timeout=None)
+httpx.patch('https://gmail.com')
+httpx.patch('https://gmail.com', timeout=None)
+httpx.options('https://gmail.com')
+httpx.options('https://gmail.com', timeout=None)
+httpx.head('https://gmail.com')
+httpx.head('https://gmail.com', timeout=None)
+httpx.Client()
+httpx.Client(timeout=None)
+httpx.AsyncClient()
+httpx.AsyncClient(timeout=None)
+with httpx.Client() as client:
+    client.get('https://gmail.com')
+with httpx.Client(timeout=None) as client:
+    client.get('https://gmail.com')
+async with httpx.AsyncClient() as client:
+    await client.get('https://gmail.com')
+async with httpx.AsyncClient(timeout=None) as client:
+    await client.get('https://gmail.com')
 
 # Okay
 not_requests.get('https://gmail.com')
+requests.get('https://gmail.com', timeout=5)
+requests.post('https://gmail.com', timeout=5)
+requests.put('https://gmail.com', timeout=5)
+requests.delete('https://gmail.com', timeout=5)
+requests.patch('https://gmail.com', timeout=5)
+requests.options('https://gmail.com', timeout=5)
+requests.head('https://gmail.com', timeout=5)
+httpx.get('https://gmail.com', timeout=5)
+httpx.post('https://gmail.com', timeout=5)
+httpx.put('https://gmail.com', timeout=5)
+httpx.delete('https://gmail.com', timeout=5)
+httpx.patch('https://gmail.com', timeout=5)
+httpx.options('https://gmail.com', timeout=5)
+httpx.head('https://gmail.com', timeout=5)
+httpx.Client(timeout=5)
+httpx.AsyncClient(timeout=5)
+with httpx.Client(timeout=5) as client:
+    client.get('https://gmail.com')
+async with httpx.AsyncClient(timeout=5) as client:
+    await client.get('https://gmail.com')
diff --git a/examples/requests-ssl-verify-disabled.py b/examples/requests-ssl-verify-disabled.py
index 25f5ef4..c45b9e9 100644
--- a/examples/requests-ssl-verify-disabled.py
+++ b/examples/requests-ssl-verify-disabled.py
@@ -1,6 +1,7 @@
 import httpx
 import requests
 
+# Errors
 requests.get('https://gmail.com', timeout=30, verify=True)
 requests.get('https://gmail.com', timeout=30, verify=False)
 requests.post('https://gmail.com', timeout=30, verify=True)
@@ -16,25 +17,26 @@ requests.options('https://gmail.com', timeout=30, verify=False)
 requests.head('https://gmail.com', timeout=30, verify=True)
 requests.head('https://gmail.com', timeout=30, verify=False)
 
-httpx.request('GET', 'https://gmail.com', verify=True)
-httpx.request('GET', 'https://gmail.com', verify=False)
-httpx.get('https://gmail.com', verify=True)
-httpx.get('https://gmail.com', verify=False)
-httpx.options('https://gmail.com', verify=True)
-httpx.options('https://gmail.com', verify=False)
-httpx.head('https://gmail.com', verify=True)
-httpx.head('https://gmail.com', verify=False)
-httpx.post('https://gmail.com', verify=True)
-httpx.post('https://gmail.com', verify=False)
-httpx.put('https://gmail.com', verify=True)
-httpx.put('https://gmail.com', verify=False)
-httpx.patch('https://gmail.com', verify=True)
-httpx.patch('https://gmail.com', verify=False)
-httpx.delete('https://gmail.com', verify=True)
-httpx.delete('https://gmail.com', verify=False)
-httpx.stream('https://gmail.com', verify=True)
-httpx.stream('https://gmail.com', verify=False)
-httpx.Client()
-httpx.Client(verify=False)
-httpx.AsyncClient()
-httpx.AsyncClient(verify=False)
+# Okay
+httpx.request('GET', 'https://gmail.com', timeout=30, verify=True)
+httpx.request('GET', 'https://gmail.com', timeout=30, verify=False)
+httpx.get('https://gmail.com', timeout=30, verify=True)
+httpx.get('https://gmail.com', timeout=30, verify=False)
+httpx.options('https://gmail.com', timeout=30, verify=True)
+httpx.options('https://gmail.com', timeout=30, verify=False)
+httpx.head('https://gmail.com', timeout=30, verify=True)
+httpx.head('https://gmail.com', timeout=30, verify=False)
+httpx.post('https://gmail.com', timeout=30, verify=True)
+httpx.post('https://gmail.com', timeout=30, verify=False)
+httpx.put('https://gmail.com', timeout=30, verify=True)
+httpx.put('https://gmail.com', timeout=30, verify=False)
+httpx.patch('https://gmail.com', timeout=30, verify=True)
+httpx.patch('https://gmail.com', timeout=30, verify=False)
+httpx.delete('https://gmail.com', timeout=30, verify=True)
+httpx.delete('https://gmail.com', timeout=30, verify=False)
+httpx.stream('https://gmail.com', timeout=30, verify=True)
+httpx.stream('https://gmail.com', timeout=30, verify=False)
+httpx.Client(timeout=30)
+httpx.Client(timeout=30, verify=False)
+httpx.AsyncClient(timeout=30)
+httpx.AsyncClient(timeout=30, verify=False)


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index 4597f70..681e45e 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -411,8 +411,8 @@ class FunctionalTests(testtools.TestCase):
     def test_requests_without_timeout(self):
         """Test for the `requests` library missing timeouts."""
         expect = {
-            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 14, "HIGH": 0},
-            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 14, "MEDIUM": 0, "HIGH": 0},
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 36, "HIGH": 0},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 36, "MEDIUM": 0, "HIGH": 0},
         }
         self.check_example("requests-missing-timeout.py", expect)