From 67ba25122350e8db1e44ed33b3ecd1b05ca97be4 Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Tue, 25 Jun 2024 16:06:21 +0200 Subject: [PATCH] feat(plugins): add support for `httpx` in `B113` (#1060) * refactor: move `HTTP_VERBS`/`HTTPX_ATTRS` to `core.utils` * perf(core): use sets for `HTTP_REQUEST_VERBS`/`HTTPX_ATTRS` * feat(plugins): add support for `httpx` in `B113` * refactor: put back `HTTP_VERBS`/`HTTPX_ATTRS` into plugins * Update bandit/plugins/request_without_timeout.py * Update bandit/plugins/request_without_timeout.py * Update bandit/plugins/request_without_timeout.py --------- Co-authored-by: Eric Brown --- .../crypto_request_no_cert_validation.py | 4 +- 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 +- 5 files changed, 93 insertions(+), 40 deletions(-) diff --git a/bandit/plugins/crypto_request_no_cert_validation.py b/bandit/plugins/crypto_request_no_cert_validation.py index 223d421ff..11791ed1e 100644 --- a/bandit/plugins/crypto_request_no_cert_validation.py +++ b/bandit/plugins/crypto_request_no_cert_validation.py @@ -54,8 +54,8 @@ @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 a418b6cc0..d571a49ea 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,6 +43,9 @@ .. 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 @@ @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 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 4597f7023..681e45edf 100644 --- a/tests/functional/test_functional.py +++ b/tests/functional/test_functional.py @@ -411,8 +411,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)