From 5623f06896c240630643465bd43ab8e875fcc357 Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Wed, 20 Sep 2023 00:04:55 +0200 Subject: [PATCH 1/7] refactor: move `HTTP_VERBS`/`HTTPX_ATTRS` to `core.utils` --- bandit/core/utils.py | 15 +++++++++++++++ .../plugins/crypto_request_no_cert_validation.py | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/bandit/core/utils.py b/bandit/core/utils.py index 32d9d4965..0b726692b 100644 --- a/bandit/core/utils.py +++ b/bandit/core/utils.py @@ -14,6 +14,21 @@ LOG = logging.getLogger(__name__) +HTTP_REQUEST_VERBS = ( + "get", + "options", + "head", + "post", + "put", + "patch", + "delete", +) +HTTPX_ATTRS = ( + "request", + "stream", + "Client", + "AsyncClient", +) + HTTP_REQUEST_VERBS """Various helper functions.""" diff --git a/bandit/plugins/crypto_request_no_cert_validation.py b/bandit/plugins/crypto_request_no_cert_validation.py index 223d421ff..4209137cb 100644 --- a/bandit/plugins/crypto_request_no_cert_validation.py +++ b/bandit/plugins/crypto_request_no_cert_validation.py @@ -49,18 +49,18 @@ import bandit from bandit.core import issue from bandit.core import test_properties as test +from bandit.core.utils import HTTP_REQUEST_VERBS +from bandit.core.utils import HTTPX_ATTRS @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 qualname = context.call_function_name_qual.split(".")[0] if ( qualname == "requests" - and context.call_function_name in HTTP_VERBS + and context.call_function_name in HTTP_REQUEST_VERBS or qualname == "httpx" and context.call_function_name in HTTPX_ATTRS ): From 277b04c29f6a3be374edb05429679f3a23d46cba Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Wed, 20 Sep 2023 00:26:56 +0200 Subject: [PATCH 2/7] perf(core): use sets for `HTTP_REQUEST_VERBS`/`HTTPX_ATTRS` --- bandit/core/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bandit/core/utils.py b/bandit/core/utils.py index 0b726692b..bc5625f98 100644 --- a/bandit/core/utils.py +++ b/bandit/core/utils.py @@ -14,7 +14,7 @@ LOG = logging.getLogger(__name__) -HTTP_REQUEST_VERBS = ( +HTTP_REQUEST_VERBS = { "get", "options", "head", @@ -22,13 +22,13 @@ "put", "patch", "delete", -) -HTTPX_ATTRS = ( +} +HTTPX_ATTRS = HTTP_REQUEST_VERBS | { "request", "stream", "Client", "AsyncClient", -) + HTTP_REQUEST_VERBS +} """Various helper functions.""" From 77d290137252d523a42c3af6af4f0f5398b7f589 Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Wed, 20 Sep 2023 00:05:25 +0200 Subject: [PATCH 3/7] feat(plugins): add support for `httpx` in `B113` --- bandit/plugins/request_without_timeout.py | 24 +++++++--- examples/requests-missing-timeout.py | 55 ++++++++++++++++++++--- examples/requests-ssl-verify-disabled.py | 46 ++++++++++--------- tests/functional/test_functional.py | 4 +- 4 files changed, 91 insertions(+), 38 deletions(-) diff --git a/bandit/plugins/request_without_timeout.py b/bandit/plugins/request_without_timeout.py index a418b6cc0..8754b5412 100644 --- a/bandit/plugins/request_without_timeout.py +++ b/bandit/plugins/request_without_timeout.py @@ -4,7 +4,8 @@ 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 @@ .. 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 @@ 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,26 +43,35 @@ .. versionadded:: 1.7.5 +.. versionchanged:: 1.7.6 + Added check for httpx module + """ # noqa: E501 import bandit from bandit.core import issue from bandit.core import test_properties as test +from bandit.core.utils import HTTP_REQUEST_VERBS +from bandit.core.utils import HTTPX_ATTRS @test.checks("Call") @test.test_id("B113") def request_without_timeout(context): - http_verbs = ("get", "options", "head", "post", "put", "patch", "delete") 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_REQUEST_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="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="Call to {qualname} with timeout set to None", ) diff --git a/examples/requests-missing-timeout.py b/examples/requests-missing-timeout.py index 38f24440a..fa71c4b0e 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 25f5ef41f..c45b9e944 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.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) diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py index 7835e7488..38341cd99 100644 --- a/tests/functional/test_functional.py +++ b/tests/functional/test_functional.py @@ -412,8 +412,8 @@ def test_requests_ssl_verify_disabled(self): 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) From 4fc4f19e2d6046796db4f76b98637aa1e23733c0 Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Fri, 22 Sep 2023 18:30:11 +0200 Subject: [PATCH 4/7] refactor: put back `HTTP_VERBS`/`HTTPX_ATTRS` into plugins --- bandit/core/utils.py | 15 --------------- .../plugins/crypto_request_no_cert_validation.py | 6 +++--- bandit/plugins/request_without_timeout.py | 6 +++--- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/bandit/core/utils.py b/bandit/core/utils.py index bc5625f98..32d9d4965 100644 --- a/bandit/core/utils.py +++ b/bandit/core/utils.py @@ -14,21 +14,6 @@ LOG = logging.getLogger(__name__) -HTTP_REQUEST_VERBS = { - "get", - "options", - "head", - "post", - "put", - "patch", - "delete", -} -HTTPX_ATTRS = HTTP_REQUEST_VERBS | { - "request", - "stream", - "Client", - "AsyncClient", -} """Various helper functions.""" diff --git a/bandit/plugins/crypto_request_no_cert_validation.py b/bandit/plugins/crypto_request_no_cert_validation.py index 4209137cb..11791ed1e 100644 --- a/bandit/plugins/crypto_request_no_cert_validation.py +++ b/bandit/plugins/crypto_request_no_cert_validation.py @@ -49,18 +49,18 @@ import bandit from bandit.core import issue from bandit.core import test_properties as test -from bandit.core.utils import HTTP_REQUEST_VERBS -from bandit.core.utils import HTTPX_ATTRS @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 qualname = context.call_function_name_qual.split(".")[0] if ( qualname == "requests" - and context.call_function_name in HTTP_REQUEST_VERBS + and context.call_function_name in HTTP_VERBS or qualname == "httpx" and context.call_function_name in HTTPX_ATTRS ): diff --git a/bandit/plugins/request_without_timeout.py b/bandit/plugins/request_without_timeout.py index 8754b5412..f64a098dc 100644 --- a/bandit/plugins/request_without_timeout.py +++ b/bandit/plugins/request_without_timeout.py @@ -50,18 +50,18 @@ import bandit from bandit.core import issue from bandit.core import test_properties as test -from bandit.core.utils import HTTP_REQUEST_VERBS -from bandit.core.utils import HTTPX_ATTRS @test.checks("Call") @test.test_id("B113") def request_without_timeout(context): + 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_REQUEST_VERBS + and context.call_function_name in HTTP_VERBS or qualname == "httpx" and context.call_function_name in HTTPX_ATTRS ): From 0ee786cbece02680f097206bcc80da6056180351 Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Mon, 24 Jun 2024 20:21:23 -0700 Subject: [PATCH 5/7] Update bandit/plugins/request_without_timeout.py --- bandit/plugins/request_without_timeout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bandit/plugins/request_without_timeout.py b/bandit/plugins/request_without_timeout.py index f64a098dc..380653136 100644 --- a/bandit/plugins/request_without_timeout.py +++ b/bandit/plugins/request_without_timeout.py @@ -43,7 +43,7 @@ .. versionadded:: 1.7.5 -.. versionchanged:: 1.7.6 +.. versionchanged:: 1.7.10 Added check for httpx module """ # noqa: E501 From 9920484b8a5403874b78ef1da32fe46bcb7fe405 Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Mon, 24 Jun 2024 20:27:21 -0700 Subject: [PATCH 6/7] Update bandit/plugins/request_without_timeout.py --- bandit/plugins/request_without_timeout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bandit/plugins/request_without_timeout.py b/bandit/plugins/request_without_timeout.py index 380653136..c638b0462 100644 --- a/bandit/plugins/request_without_timeout.py +++ b/bandit/plugins/request_without_timeout.py @@ -71,7 +71,7 @@ def request_without_timeout(context): severity=bandit.MEDIUM, confidence=bandit.LOW, cwe=issue.Cwe.UNCONTROLLED_RESOURCE_CONSUMPTION, - text="Call to {qualname} without timeout", + text=f"Call to {qualname} without timeout", ) # check for timeout=None if context.check_call_arg_value("timeout", "None"): From a1e7d7f515efdddc338cd5a1918d0948a31196a5 Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Mon, 24 Jun 2024 20:27:27 -0700 Subject: [PATCH 7/7] Update bandit/plugins/request_without_timeout.py --- bandit/plugins/request_without_timeout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bandit/plugins/request_without_timeout.py b/bandit/plugins/request_without_timeout.py index c638b0462..d571a49ea 100644 --- a/bandit/plugins/request_without_timeout.py +++ b/bandit/plugins/request_without_timeout.py @@ -79,5 +79,5 @@ def request_without_timeout(context): severity=bandit.MEDIUM, confidence=bandit.LOW, cwe=issue.Cwe.UNCONTROLLED_RESOURCE_CONSUMPTION, - text="Call to {qualname} with timeout set to None", + text=f"Call to {qualname} with timeout set to None", )