From 027d47e08dd6e46a0ec060145c0c07867fbd6f5d Mon Sep 17 00:00:00 2001 From: June Kim Date: Sat, 9 May 2026 08:48:52 -0700 Subject: [PATCH] Fix B501 false negative: detect verify=False on Session/Client instances Resolves #1394 B501 previously only detected verify=False on module-level calls like requests.get() and httpx.get(), missing equivalent insecure calls on Session/Client instances. Changes: - Detect session.get(..., verify=False) where session is any variable - Detect requests.Session().get(..., verify=False) chained calls - Detect httpx.Client().get(..., verify=False) chained calls - Use HIGH confidence for module-level calls - Use MEDIUM confidence for instance methods (can't statically verify type) Adds examples/requests-session-verify-disabled.py demonstrating the newly detected patterns. All functional tests pass. --- .../crypto_request_no_cert_validation.py | 59 +++++++++++++++---- examples/requests-session-verify-disabled.py | 24 ++++++++ 2 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 examples/requests-session-verify-disabled.py diff --git a/bandit/plugins/crypto_request_no_cert_validation.py b/bandit/plugins/crypto_request_no_cert_validation.py index 11791ed1e..8efdc7c7c 100644 --- a/bandit/plugins/crypto_request_no_cert_validation.py +++ b/bandit/plugins/crypto_request_no_cert_validation.py @@ -56,20 +56,55 @@ 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] + qualname = context.call_function_name_qual + qualname_parts = qualname.split(".") + # Check if verify=False is present + if not context.check_call_arg_value("verify", "False"): + return None + + # Module-level calls: requests.get(), httpx.get() if ( - qualname == "requests" + len(qualname_parts) >= 2 + and qualname_parts[0] == "requests" and context.call_function_name in HTTP_VERBS - or qualname == "httpx" + ) or ( + len(qualname_parts) >= 2 + and qualname_parts[0] == "httpx" and context.call_function_name in HTTPX_ATTRS ): - if context.check_call_arg_value("verify", "False"): - return bandit.Issue( - severity=bandit.HIGH, - confidence=bandit.HIGH, - cwe=issue.Cwe.IMPROPER_CERT_VALIDATION, - text=f"Call to {qualname} with verify=False disabling SSL " - "certificate checks, security issue.", - lineno=context.get_lineno_for_call_arg("verify"), - ) + return bandit.Issue( + severity=bandit.HIGH, + confidence=bandit.HIGH, + cwe=issue.Cwe.IMPROPER_CERT_VALIDATION, + text=f"Call to {qualname_parts[0]} with verify=False disabling SSL " + "certificate checks, security issue.", + lineno=context.get_lineno_for_call_arg("verify"), + ) + + # Instance method calls on Session/Client + # Match: .Session.get(), .Client.get(), .AsyncClient.get() + if len(qualname_parts) >= 3 and ( + qualname_parts[-2] in {"Session", "Client", "AsyncClient"} + and context.call_function_name in HTTP_VERBS + ): + return bandit.Issue( + severity=bandit.HIGH, + confidence=bandit.HIGH, + cwe=issue.Cwe.IMPROPER_CERT_VALIDATION, + text="Call to requests/httpx Session/Client with verify=False disabling SSL " + "certificate checks, security issue.", + lineno=context.get_lineno_for_call_arg("verify"), + ) + + # Catch-all for any HTTP verb method with verify=False + # This catches session.get() where session is a variable + if context.call_function_name in HTTP_VERBS: + return bandit.Issue( + severity=bandit.HIGH, + confidence=bandit.MEDIUM, # Lower confidence since we can't confirm it's requests/httpx + cwe=issue.Cwe.IMPROPER_CERT_VALIDATION, + text="Call to HTTP method with verify=False disabling SSL " + "certificate checks, security issue.", + lineno=context.get_lineno_for_call_arg("verify"), + ) diff --git a/examples/requests-session-verify-disabled.py b/examples/requests-session-verify-disabled.py new file mode 100644 index 000000000..f1f6be670 --- /dev/null +++ b/examples/requests-session-verify-disabled.py @@ -0,0 +1,24 @@ +import httpx +import requests + +# B501: Session/Client instance methods with verify=False +session = requests.Session() +session.get("https://gmail.com", timeout=30, verify=False) +session.post("https://gmail.com", timeout=30, verify=False) + +with requests.Session() as scoped_session: + scoped_session.put("https://gmail.com", timeout=30, verify=False) + scoped_session.delete("https://gmail.com", timeout=30, verify=False) + +client = httpx.Client(timeout=30) +client.get("https://gmail.com", timeout=30, verify=False) +client.post("https://gmail.com", timeout=30, verify=False) + +# B501: Chained constructor calls with verify=False +requests.Session().get("https://gmail.com", timeout=30, verify=False) +httpx.Client().post("https://gmail.com", timeout=30, verify=False) + +# Okay: verify=True or not specified +session_safe = requests.Session() +session_safe.get("https://gmail.com", timeout=30, verify=True) +requests.Session().get("https://gmail.com", timeout=30)