This commit is contained in:
Iliyan Angelov
2025-12-01 06:50:10 +02:00
parent 91f51bc6fe
commit 62c1fe5951
4682 changed files with 544807 additions and 31208 deletions

View File

@@ -0,0 +1,63 @@
#
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
======================================================
B201: Test for use of flask app with debug set to true
======================================================
Running Flask applications in debug mode results in the Werkzeug debugger
being enabled. This includes a feature that allows arbitrary code execution.
Documentation for both Flask [1]_ and Werkzeug [2]_ strongly suggests that
debug mode should never be enabled on production systems.
Operating a production server with debug mode enabled was the probable cause
of the Patreon breach in 2015 [3]_.
:Example:
.. code-block:: none
>> Issue: A Flask app appears to be run with debug=True, which exposes
the Werkzeug debugger and allows the execution of arbitrary code.
Severity: High Confidence: High
CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html)
Location: examples/flask_debug.py:10
9 #bad
10 app.run(debug=True)
11
.. seealso::
.. [1] https://flask.palletsprojects.com/en/1.1.x/quickstart/#debug-mode
.. [2] https://werkzeug.palletsprojects.com/en/1.0.x/debug/
.. [3] https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/
.. https://cwe.mitre.org/data/definitions/94.html
.. versionadded:: 0.15.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.test_id("B201")
@test.checks("Call")
def flask_debug_true(context):
if context.is_module_imported_like("flask"):
if context.call_function_name_qual.endswith(".run"):
if context.check_call_arg_value("debug", "True"):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.CODE_INJECTION,
text="A Flask app appears to be run with debug=True, "
"which exposes the Werkzeug debugger and allows "
"the execution of arbitrary code.",
lineno=context.get_lineno_for_call_arg("debug"),
)

View File

@@ -0,0 +1,83 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
============================
B101: Test for use of assert
============================
This plugin test checks for the use of the Python ``assert`` keyword. It was
discovered that some projects used assert to enforce interface constraints.
However, assert is removed with compiling to optimised byte code (`python -O`
producing \*.opt-1.pyc files). This caused various protections to be removed.
Consider raising a semantically meaningful error or ``AssertionError`` instead.
Please see
https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement for
more info on ``assert``.
**Config Options:**
You can configure files that skip this check. This is often useful when you
use assert statements in test cases.
.. code-block:: yaml
assert_used:
skips: ['*_test.py', '*test_*.py']
:Example:
.. code-block:: none
>> Issue: Use of assert detected. The enclosed code will be removed when
compiling to optimised byte code.
Severity: Low Confidence: High
CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
Location: ./examples/assert.py:1
1 assert logged_in
2 display_assets()
.. seealso::
- https://bugs.launchpad.net/juniperopenstack/+bug/1456193
- https://bugs.launchpad.net/heat/+bug/1397883
- https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement
- https://cwe.mitre.org/data/definitions/703.html
.. versionadded:: 0.11.0
.. versionchanged:: 1.7.3
CWE information added
"""
import fnmatch
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def gen_config(name):
if name == "assert_used":
return {"skips": []}
@test.takes_config
@test.test_id("B101")
@test.checks("Assert")
def assert_used(context, config):
for skip in config.get("skips", []):
if fnmatch.fnmatch(context.filename, skip):
return None
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
cwe=issue.Cwe.IMPROPER_CHECK_OF_EXCEPT_COND,
text=(
"Use of assert detected. The enclosed code "
"will be removed when compiling to optimised byte code."
),
)

View File

@@ -0,0 +1,75 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
=============================================
B501: Test for missing certificate validation
=============================================
Encryption in general is typically critical to the security of many
applications. Using TLS can greatly increase security by guaranteeing the
identity of the party you are communicating with. This is accomplished by one
or both parties presenting trusted certificates during the connection
initialization phase of TLS.
When HTTPS request methods are used, certificates are validated automatically
which is the desired behavior. If certificate validation is explicitly turned
off Bandit will return a HIGH severity error.
:Example:
.. code-block:: none
>> Issue: [request_with_no_cert_validation] Call to requests with
verify=False disabling SSL certificate checks, security issue.
Severity: High Confidence: High
CWE: CWE-295 (https://cwe.mitre.org/data/definitions/295.html)
Location: examples/requests-ssl-verify-disabled.py:4
3 requests.get('https://gmail.com', verify=True)
4 requests.get('https://gmail.com', verify=False)
5 requests.post('https://gmail.com', verify=True)
.. seealso::
- https://security.openstack.org/guidelines/dg_move-data-securely.html
- https://security.openstack.org/guidelines/dg_validate-certificates.html
- https://cwe.mitre.org/data/definitions/295.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
.. versionchanged:: 1.7.5
Added check for httpx module
"""
import bandit
from bandit.core import issue
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
qualname = context.call_function_name_qual.split(".")[0]
if (
qualname == "requests"
and context.call_function_name in HTTP_VERBS
or qualname == "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"),
)

View File

@@ -0,0 +1,155 @@
#
# Copyright (C) 2018 [Victor Torre](https://github.com/ehooo)
#
# SPDX-License-Identifier: Apache-2.0
import ast
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def keywords2dict(keywords):
kwargs = {}
for node in keywords:
if isinstance(node, ast.keyword):
kwargs[node.arg] = node.value
return kwargs
@test.checks("Call")
@test.test_id("B610")
def django_extra_used(context):
"""**B610: Potential SQL injection on extra function**
:Example:
.. code-block:: none
>> Issue: [B610:django_extra_used] Use of extra potential SQL attack vector.
Severity: Medium Confidence: Medium
CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
Location: examples/django_sql_injection_extra.py:29:0
More Info: https://bandit.readthedocs.io/en/latest/plugins/b610_django_extra_used.html
28 tables_str = 'django_content_type" WHERE "auth_user"."username"="admin'
29 User.objects.all().extra(tables=[tables_str]).distinct()
.. seealso::
- https://docs.djangoproject.com/en/dev/topics/security/\
#sql-injection-protection
- https://cwe.mitre.org/data/definitions/89.html
.. versionadded:: 1.5.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
description = "Use of extra potential SQL attack vector."
if context.call_function_name == "extra":
kwargs = keywords2dict(context.node.keywords)
args = context.node.args
if args:
if len(args) >= 1:
kwargs["select"] = args[0]
if len(args) >= 2:
kwargs["where"] = args[1]
if len(args) >= 3:
kwargs["params"] = args[2]
if len(args) >= 4:
kwargs["tables"] = args[3]
if len(args) >= 5:
kwargs["order_by"] = args[4]
if len(args) >= 6:
kwargs["select_params"] = args[5]
insecure = False
for key in ["where", "tables"]:
if key in kwargs:
if isinstance(kwargs[key], ast.List):
for val in kwargs[key].elts:
if not (
isinstance(val, ast.Constant)
and isinstance(val.value, str)
):
insecure = True
break
else:
insecure = True
break
if not insecure and "select" in kwargs:
if isinstance(kwargs["select"], ast.Dict):
for k in kwargs["select"].keys:
if not (
isinstance(k, ast.Constant)
and isinstance(k.value, str)
):
insecure = True
break
if not insecure:
for v in kwargs["select"].values:
if not (
isinstance(v, ast.Constant)
and isinstance(v.value, str)
):
insecure = True
break
else:
insecure = True
if insecure:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.SQL_INJECTION,
text=description,
)
@test.checks("Call")
@test.test_id("B611")
def django_rawsql_used(context):
"""**B611: Potential SQL injection on RawSQL function**
:Example:
.. code-block:: none
>> Issue: [B611:django_rawsql_used] Use of RawSQL potential SQL attack vector.
Severity: Medium Confidence: Medium
CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
Location: examples/django_sql_injection_raw.py:11:26
More Info: https://bandit.readthedocs.io/en/latest/plugins/b611_django_rawsql_used.html
10 ' WHERE "username"="admin" OR 1=%s --'
11 User.objects.annotate(val=RawSQL(raw, [0]))
.. seealso::
- https://docs.djangoproject.com/en/dev/topics/security/\
#sql-injection-protection
- https://cwe.mitre.org/data/definitions/89.html
.. versionadded:: 1.5.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
description = "Use of RawSQL potential SQL attack vector."
if context.is_module_imported_like("django.db.models"):
if context.call_function_name == "RawSQL":
if context.node.args:
sql = context.node.args[0]
else:
kwargs = keywords2dict(context.node.keywords)
sql = kwargs["sql"]
if not (
isinstance(sql, ast.Constant) and isinstance(sql.value, str)
):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.SQL_INJECTION,
text=description,
)

View File

@@ -0,0 +1,287 @@
#
# Copyright 2018 Victor Torre
#
# SPDX-License-Identifier: Apache-2.0
import ast
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
class DeepAssignation:
def __init__(self, var_name, ignore_nodes=None):
self.var_name = var_name
self.ignore_nodes = ignore_nodes
def is_assigned_in(self, items):
assigned = []
for ast_inst in items:
new_assigned = self.is_assigned(ast_inst)
if new_assigned:
if isinstance(new_assigned, (list, tuple)):
assigned.extend(new_assigned)
else:
assigned.append(new_assigned)
return assigned
def is_assigned(self, node):
assigned = False
if self.ignore_nodes:
if isinstance(self.ignore_nodes, (list, tuple, object)):
if isinstance(node, self.ignore_nodes):
return assigned
if isinstance(node, ast.Expr):
assigned = self.is_assigned(node.value)
elif isinstance(node, ast.FunctionDef):
for name in node.args.args:
if isinstance(name, ast.Name):
if name.id == self.var_name.id:
# If is param the assignations are not affected
return assigned
assigned = self.is_assigned_in(node.body)
elif isinstance(node, ast.With):
for withitem in node.items:
var_id = getattr(withitem.optional_vars, "id", None)
if var_id == self.var_name.id:
assigned = node
else:
assigned = self.is_assigned_in(node.body)
elif isinstance(node, ast.Try):
assigned = []
assigned.extend(self.is_assigned_in(node.body))
assigned.extend(self.is_assigned_in(node.handlers))
assigned.extend(self.is_assigned_in(node.orelse))
assigned.extend(self.is_assigned_in(node.finalbody))
elif isinstance(node, ast.ExceptHandler):
assigned = []
assigned.extend(self.is_assigned_in(node.body))
elif isinstance(node, (ast.If, ast.For, ast.While)):
assigned = []
assigned.extend(self.is_assigned_in(node.body))
assigned.extend(self.is_assigned_in(node.orelse))
elif isinstance(node, ast.AugAssign):
if isinstance(node.target, ast.Name):
if node.target.id == self.var_name.id:
assigned = node.value
elif isinstance(node, ast.Assign) and node.targets:
target = node.targets[0]
if isinstance(target, ast.Name):
if target.id == self.var_name.id:
assigned = node.value
elif isinstance(target, ast.Tuple) and isinstance(
node.value, ast.Tuple
):
pos = 0
for name in target.elts:
if name.id == self.var_name.id:
assigned = node.value.elts[pos]
break
pos += 1
return assigned
def evaluate_var(xss_var, parent, until, ignore_nodes=None):
secure = False
if isinstance(xss_var, ast.Name):
if isinstance(parent, ast.FunctionDef):
for name in parent.args.args:
if name.arg == xss_var.id:
return False # Params are not secure
analyser = DeepAssignation(xss_var, ignore_nodes)
for node in parent.body:
if node.lineno >= until:
break
to = analyser.is_assigned(node)
if to:
if isinstance(to, ast.Constant) and isinstance(to.value, str):
secure = True
elif isinstance(to, ast.Name):
secure = evaluate_var(to, parent, to.lineno, ignore_nodes)
elif isinstance(to, ast.Call):
secure = evaluate_call(to, parent, ignore_nodes)
elif isinstance(to, (list, tuple)):
num_secure = 0
for some_to in to:
if isinstance(some_to, ast.Constant) and isinstance(
some_to.value, str
):
num_secure += 1
elif isinstance(some_to, ast.Name):
if evaluate_var(
some_to, parent, node.lineno, ignore_nodes
):
num_secure += 1
else:
break
else:
break
if num_secure == len(to):
secure = True
else:
secure = False
break
else:
secure = False
break
return secure
def evaluate_call(call, parent, ignore_nodes=None):
secure = False
evaluate = False
if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute):
if (
isinstance(call.func.value, ast.Constant)
and call.func.attr == "format"
):
evaluate = True
if call.keywords:
evaluate = False # TODO(??) get support for this
if evaluate:
args = list(call.args)
num_secure = 0
for arg in args:
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
num_secure += 1
elif isinstance(arg, ast.Name):
if evaluate_var(arg, parent, call.lineno, ignore_nodes):
num_secure += 1
else:
break
elif isinstance(arg, ast.Call):
if evaluate_call(arg, parent, ignore_nodes):
num_secure += 1
else:
break
elif isinstance(arg, ast.Starred) and isinstance(
arg.value, (ast.List, ast.Tuple)
):
args.extend(arg.value.elts)
num_secure += 1
else:
break
secure = num_secure == len(args)
return secure
def transform2call(var):
if isinstance(var, ast.BinOp):
is_mod = isinstance(var.op, ast.Mod)
is_left_str = isinstance(var.left, ast.Constant) and isinstance(
var.left.value, str
)
if is_mod and is_left_str:
new_call = ast.Call()
new_call.args = []
new_call.args = []
new_call.keywords = None
new_call.lineno = var.lineno
new_call.func = ast.Attribute()
new_call.func.value = var.left
new_call.func.attr = "format"
if isinstance(var.right, ast.Tuple):
new_call.args = var.right.elts
else:
new_call.args = [var.right]
return new_call
def check_risk(node):
description = "Potential XSS on mark_safe function."
xss_var = node.args[0]
secure = False
if isinstance(xss_var, ast.Name):
# Check if the var are secure
parent = node._bandit_parent
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
parent = parent._bandit_parent
is_param = False
if isinstance(parent, ast.FunctionDef):
for name in parent.args.args:
if name.arg == xss_var.id:
is_param = True
break
if not is_param:
secure = evaluate_var(xss_var, parent, node.lineno)
elif isinstance(xss_var, ast.Call):
parent = node._bandit_parent
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
parent = parent._bandit_parent
secure = evaluate_call(xss_var, parent)
elif isinstance(xss_var, ast.BinOp):
is_mod = isinstance(xss_var.op, ast.Mod)
is_left_str = isinstance(xss_var.left, ast.Constant) and isinstance(
xss_var.left.value, str
)
if is_mod and is_left_str:
parent = node._bandit_parent
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
parent = parent._bandit_parent
new_call = transform2call(xss_var)
secure = evaluate_call(new_call, parent)
if not secure:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.BASIC_XSS,
text=description,
)
@test.checks("Call")
@test.test_id("B703")
def django_mark_safe(context):
"""**B703: Potential XSS on mark_safe function**
:Example:
.. code-block:: none
>> Issue: [B703:django_mark_safe] Potential XSS on mark_safe function.
Severity: Medium Confidence: High
CWE: CWE-80 (https://cwe.mitre.org/data/definitions/80.html)
Location: examples/mark_safe_insecure.py:159:4
More Info: https://bandit.readthedocs.io/en/latest/plugins/b703_django_mark_safe.html
158 str_arg = 'could be insecure'
159 safestring.mark_safe(str_arg)
.. seealso::
- https://docs.djangoproject.com/en/dev/topics/security/\
#cross-site-scripting-xss-protection
- https://docs.djangoproject.com/en/dev/ref/utils/\
#module-django.utils.safestring
- https://docs.djangoproject.com/en/dev/ref/utils/\
#django.utils.html.format_html
- https://cwe.mitre.org/data/definitions/80.html
.. versionadded:: 1.5.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
if context.is_module_imported_like("django.utils.safestring"):
affected_functions = [
"mark_safe",
"SafeText",
"SafeUnicode",
"SafeString",
"SafeBytes",
]
if context.call_function_name in affected_functions:
xss = context.node.args[0]
if not (
isinstance(xss, ast.Constant) and isinstance(xss.value, str)
):
return check_risk(context.node)

View File

@@ -0,0 +1,55 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
==============================
B102: Test for the use of exec
==============================
This plugin test checks for the use of Python's `exec` method or keyword. The
Python docs succinctly describe why the use of `exec` is risky.
:Example:
.. code-block:: none
>> Issue: Use of exec detected.
Severity: Medium Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/exec.py:2
1 exec("do evil")
.. seealso::
- https://docs.python.org/3/library/functions.html#exec
- https://www.python.org/dev/peps/pep-0551/#background
- https://www.python.org/dev/peps/pep-0578/#suggested-audit-hook-locations
- https://cwe.mitre.org/data/definitions/78.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
"""
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def exec_issue():
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text="Use of exec detected.",
)
@test.checks("Call")
@test.test_id("B102")
def exec_used(context):
if context.call_function_name_qual == "exec":
return exec_issue()

View File

@@ -0,0 +1,99 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
==================================================
B103: Test for setting permissive file permissions
==================================================
POSIX based operating systems utilize a permissions model to protect access to
parts of the file system. This model supports three roles "owner", "group"
and "world" each role may have a combination of "read", "write" or "execute"
flags sets. Python provides ``chmod`` to manipulate POSIX style permissions.
This plugin test looks for the use of ``chmod`` and will alert when it is used
to set particularly permissive control flags. A MEDIUM warning is generated if
a file is set to group write or executable and a HIGH warning is reported if a
file is set world write or executable. Warnings are given with HIGH confidence.
:Example:
.. code-block:: none
>> Issue: Probable insecure usage of temp file/directory.
Severity: Medium Confidence: Medium
CWE: CWE-732 (https://cwe.mitre.org/data/definitions/732.html)
Location: ./examples/os-chmod.py:15
14 os.chmod('/etc/hosts', 0o777)
15 os.chmod('/tmp/oh_hai', 0x1ff)
16 os.chmod('/etc/passwd', stat.S_IRWXU)
>> Issue: Chmod setting a permissive mask 0777 on file (key_file).
Severity: High Confidence: High
CWE: CWE-732 (https://cwe.mitre.org/data/definitions/732.html)
Location: ./examples/os-chmod.py:17
16 os.chmod('/etc/passwd', stat.S_IRWXU)
17 os.chmod(key_file, 0o777)
18
.. seealso::
- https://security.openstack.org/guidelines/dg_apply-restrictive-file-permissions.html
- https://en.wikipedia.org/wiki/File_system_permissions
- https://security.openstack.org
- https://cwe.mitre.org/data/definitions/732.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
.. versionchanged:: 1.7.5
Added checks for S_IWGRP and S_IXOTH
""" # noqa: E501
import stat
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def _stat_is_dangerous(mode):
return (
mode & stat.S_IWOTH
or mode & stat.S_IWGRP
or mode & stat.S_IXGRP
or mode & stat.S_IXOTH
)
@test.checks("Call")
@test.test_id("B103")
def set_bad_file_permissions(context):
if "chmod" in context.call_function_name:
if context.call_args_count == 2:
mode = context.get_call_arg_at_position(1)
if (
mode is not None
and isinstance(mode, int)
and _stat_is_dangerous(mode)
):
# world writable is an HIGH, group executable is a MEDIUM
if mode & stat.S_IWOTH:
sev_level = bandit.HIGH
else:
sev_level = bandit.MEDIUM
filename = context.get_call_arg_at_position(0)
if filename is None:
filename = "NOT PARSED"
return bandit.Issue(
severity=sev_level,
confidence=bandit.HIGH,
cwe=issue.Cwe.INCORRECT_PERMISSION_ASSIGNMENT,
text="Chmod setting a permissive mask %s on file (%s)."
% (oct(mode), filename),
)

View File

@@ -0,0 +1,52 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
========================================
B104: Test for binding to all interfaces
========================================
Binding to all network interfaces can potentially open up a service to traffic
on unintended interfaces, that may not be properly documented or secured. This
plugin test looks for a string pattern "0.0.0.0" that may indicate a hardcoded
binding to all network interfaces.
:Example:
.. code-block:: none
>> Issue: Possible binding to all interfaces.
Severity: Medium Confidence: Medium
CWE: CWE-605 (https://cwe.mitre.org/data/definitions/605.html)
Location: ./examples/binding.py:4
3 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4 s.bind(('0.0.0.0', 31137))
5 s.bind(('192.168.0.1', 8080))
.. seealso::
- https://nvd.nist.gov/vuln/detail/CVE-2018-1281
- https://cwe.mitre.org/data/definitions/605.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
"""
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.checks("Str")
@test.test_id("B104")
def hardcoded_bind_all_interfaces(context):
if context.string_val == "0.0.0.0": # nosec: B104
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.MULTIPLE_BINDS,
text="Possible binding to all interfaces.",
)

View File

@@ -0,0 +1,269 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
import ast
import re
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
RE_WORDS = "(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?)"
RE_CANDIDATES = re.compile(
"(^{0}$|_{0}_|^{0}_|_{0}$)".format(RE_WORDS), re.IGNORECASE
)
def _report(value):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.HARD_CODED_PASSWORD,
text=f"Possible hardcoded password: '{value}'",
)
@test.checks("Str")
@test.test_id("B105")
def hardcoded_password_string(context):
"""**B105: Test for use of hard-coded password strings**
The use of hard-coded passwords increases the possibility of password
guessing tremendously. This plugin test looks for all string literals and
checks the following conditions:
- assigned to a variable that looks like a password
- assigned to a dict key that looks like a password
- assigned to a class attribute that looks like a password
- used in a comparison with a variable that looks like a password
Variables are considered to look like a password if they have match any one
of:
- "password"
- "pass"
- "passwd"
- "pwd"
- "secret"
- "token"
- "secrete"
Note: this can be noisy and may generate false positives.
**Config Options:**
None
:Example:
.. code-block:: none
>> Issue: Possible hardcoded password '(root)'
Severity: Low Confidence: Low
CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
Location: ./examples/hardcoded-passwords.py:5
4 def someFunction2(password):
5 if password == "root":
6 print("OK, logged in")
.. seealso::
- https://www.owasp.org/index.php/Use_of_hard-coded_password
- https://cwe.mitre.org/data/definitions/259.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
"""
node = context.node
if isinstance(node._bandit_parent, ast.Assign):
# looks for "candidate='some_string'"
for targ in node._bandit_parent.targets:
if isinstance(targ, ast.Name) and RE_CANDIDATES.search(targ.id):
return _report(node.value)
elif isinstance(targ, ast.Attribute) and RE_CANDIDATES.search(
targ.attr
):
return _report(node.value)
elif isinstance(
node._bandit_parent, ast.Subscript
) and RE_CANDIDATES.search(node.value):
# Py39+: looks for "dict[candidate]='some_string'"
# subscript -> index -> string
assign = node._bandit_parent._bandit_parent
if (
isinstance(assign, ast.Assign)
and isinstance(assign.value, ast.Constant)
and isinstance(assign.value.value, str)
):
return _report(assign.value.value)
elif isinstance(node._bandit_parent, ast.Index) and RE_CANDIDATES.search(
node.value
):
# looks for "dict[candidate]='some_string'"
# assign -> subscript -> index -> string
assign = node._bandit_parent._bandit_parent._bandit_parent
if (
isinstance(assign, ast.Assign)
and isinstance(assign.value, ast.Constant)
and isinstance(assign.value.value, str)
):
return _report(assign.value.value)
elif isinstance(node._bandit_parent, ast.Compare):
# looks for "candidate == 'some_string'"
comp = node._bandit_parent
if isinstance(comp.left, ast.Name):
if RE_CANDIDATES.search(comp.left.id):
if isinstance(
comp.comparators[0], ast.Constant
) and isinstance(comp.comparators[0].value, str):
return _report(comp.comparators[0].value)
elif isinstance(comp.left, ast.Attribute):
if RE_CANDIDATES.search(comp.left.attr):
if isinstance(
comp.comparators[0], ast.Constant
) and isinstance(comp.comparators[0].value, str):
return _report(comp.comparators[0].value)
@test.checks("Call")
@test.test_id("B106")
def hardcoded_password_funcarg(context):
"""**B106: Test for use of hard-coded password function arguments**
The use of hard-coded passwords increases the possibility of password
guessing tremendously. This plugin test looks for all function calls being
passed a keyword argument that is a string literal. It checks that the
assigned local variable does not look like a password.
Variables are considered to look like a password if they have match any one
of:
- "password"
- "pass"
- "passwd"
- "pwd"
- "secret"
- "token"
- "secrete"
Note: this can be noisy and may generate false positives.
**Config Options:**
None
:Example:
.. code-block:: none
>> Issue: [B106:hardcoded_password_funcarg] Possible hardcoded
password: 'blerg'
Severity: Low Confidence: Medium
CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
Location: ./examples/hardcoded-passwords.py:16
15
16 doLogin(password="blerg")
.. seealso::
- https://www.owasp.org/index.php/Use_of_hard-coded_password
- https://cwe.mitre.org/data/definitions/259.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
"""
# looks for "function(candidate='some_string')"
for kw in context.node.keywords:
if (
isinstance(kw.value, ast.Constant)
and isinstance(kw.value.value, str)
and RE_CANDIDATES.search(kw.arg)
):
return _report(kw.value.value)
@test.checks("FunctionDef")
@test.test_id("B107")
def hardcoded_password_default(context):
"""**B107: Test for use of hard-coded password argument defaults**
The use of hard-coded passwords increases the possibility of password
guessing tremendously. This plugin test looks for all function definitions
that specify a default string literal for some argument. It checks that
the argument does not look like a password.
Variables are considered to look like a password if they have match any one
of:
- "password"
- "pass"
- "passwd"
- "pwd"
- "secret"
- "token"
- "secrete"
Note: this can be noisy and may generate false positives. We do not
report on None values which can be legitimately used as a default value,
when initializing a function or class.
**Config Options:**
None
:Example:
.. code-block:: none
>> Issue: [B107:hardcoded_password_default] Possible hardcoded
password: 'Admin'
Severity: Low Confidence: Medium
CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
Location: ./examples/hardcoded-passwords.py:1
1 def someFunction(user, password="Admin"):
2 print("Hi " + user)
.. seealso::
- https://www.owasp.org/index.php/Use_of_hard-coded_password
- https://cwe.mitre.org/data/definitions/259.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
"""
# looks for "def function(candidate='some_string')"
# this pads the list of default values with "None" if nothing is given
defs = [None] * (
len(context.node.args.args) - len(context.node.args.defaults)
)
defs.extend(context.node.args.defaults)
# go through all (param, value)s and look for candidates
for key, val in zip(context.node.args.args, defs):
if isinstance(key, (ast.Name, ast.arg)):
# Skip if the default value is None
if val is None or (
isinstance(val, ast.Constant) and val.value is None
):
continue
if (
isinstance(val, ast.Constant)
and isinstance(val.value, str)
and RE_CANDIDATES.search(key.arg)
):
return _report(val.value)

View File

@@ -0,0 +1,79 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
===================================================
B108: Test for insecure usage of tmp file/directory
===================================================
Safely creating a temporary file or directory means following a number of rules
(see the references for more details). This plugin test looks for strings
starting with (configurable) commonly used temporary paths, for example:
- /tmp
- /var/tmp
- /dev/shm
**Config Options:**
This test plugin takes a similarly named config block,
`hardcoded_tmp_directory`. The config block provides a Python list, `tmp_dirs`,
that lists string fragments indicating possible temporary file paths. Any
string starting with one of these fragments will report a MEDIUM confidence
issue.
.. code-block:: yaml
hardcoded_tmp_directory:
tmp_dirs: ['/tmp', '/var/tmp', '/dev/shm']
:Example:
.. code-block: none
>> Issue: Probable insecure usage of temp file/directory.
Severity: Medium Confidence: Medium
CWE: CWE-377 (https://cwe.mitre.org/data/definitions/377.html)
Location: ./examples/hardcoded-tmp.py:1
1 f = open('/tmp/abc', 'w')
2 f.write('def')
.. seealso::
- https://security.openstack.org/guidelines/dg_using-temporary-files-securely.html
- https://cwe.mitre.org/data/definitions/377.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def gen_config(name):
if name == "hardcoded_tmp_directory":
return {"tmp_dirs": ["/tmp", "/var/tmp", "/dev/shm"]} # nosec: B108
@test.takes_config
@test.checks("Str")
@test.test_id("B108")
def hardcoded_tmp_directory(context, config):
if config is not None and "tmp_dirs" in config:
tmp_dirs = config["tmp_dirs"]
else:
tmp_dirs = ["/tmp", "/var/tmp", "/dev/shm"] # nosec: B108
if any(context.string_val.startswith(s) for s in tmp_dirs):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.INSECURE_TEMP_FILE,
text="Probable insecure usage of temp file/directory.",
)

View File

@@ -0,0 +1,121 @@
#
# SPDX-License-Identifier: Apache-2.0
r"""
======================================================================
B324: Test use of insecure md4, md5, or sha1 hash functions in hashlib
======================================================================
This plugin checks for the usage of the insecure MD4, MD5, or SHA1 hash
functions in ``hashlib`` and ``crypt``. The ``hashlib.new`` function provides
the ability to construct a new hashing object using the named algorithm. This
can be used to create insecure hash functions like MD4 and MD5 if they are
passed as algorithm names to this function.
This check does additional checking for usage of keyword usedforsecurity on all
function variations of hashlib.
Similar to ``hashlib``, this plugin also checks for usage of one of the
``crypt`` module's weak hashes. ``crypt`` also permits MD5 among other weak
hash variants.
:Example:
.. code-block:: none
>> Issue: [B324:hashlib] Use of weak MD4, MD5, or SHA1 hash for
security. Consider usedforsecurity=False
Severity: High Confidence: High
CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html)
Location: examples/hashlib_new_insecure_functions.py:3:0
More Info: https://bandit.readthedocs.io/en/latest/plugins/b324_hashlib.html
2
3 hashlib.new('md5')
4
.. seealso::
- https://cwe.mitre.org/data/definitions/327.html
.. versionadded:: 1.5.0
.. versionchanged:: 1.7.3
CWE information added
.. versionchanged:: 1.7.6
Added check for the crypt module weak hashes
""" # noqa: E501
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
WEAK_HASHES = ("md4", "md5", "sha", "sha1")
WEAK_CRYPT_HASHES = ("METHOD_CRYPT", "METHOD_MD5", "METHOD_BLOWFISH")
def _hashlib_func(context, func):
keywords = context.call_keywords
if func in WEAK_HASHES:
if keywords.get("usedforsecurity", "True") == "True":
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
cwe=issue.Cwe.BROKEN_CRYPTO,
text=f"Use of weak {func.upper()} hash for security. "
"Consider usedforsecurity=False",
lineno=context.node.lineno,
)
elif func == "new":
args = context.call_args
name = args[0] if args else keywords.get("name", None)
if isinstance(name, str) and name.lower() in WEAK_HASHES:
if keywords.get("usedforsecurity", "True") == "True":
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
cwe=issue.Cwe.BROKEN_CRYPTO,
text=f"Use of weak {name.upper()} hash for "
"security. Consider usedforsecurity=False",
lineno=context.node.lineno,
)
def _crypt_crypt(context, func):
args = context.call_args
keywords = context.call_keywords
if func == "crypt":
name = args[1] if len(args) > 1 else keywords.get("salt", None)
if isinstance(name, str) and name in WEAK_CRYPT_HASHES:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.BROKEN_CRYPTO,
text=f"Use of insecure crypt.{name.upper()} hash function.",
lineno=context.node.lineno,
)
elif func == "mksalt":
name = args[0] if args else keywords.get("method", None)
if isinstance(name, str) and name in WEAK_CRYPT_HASHES:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.BROKEN_CRYPTO,
text=f"Use of insecure crypt.{name.upper()} hash function.",
lineno=context.node.lineno,
)
@test.test_id("B324")
@test.checks("Call")
def hashlib(context):
if isinstance(context.call_function_name_qual, str):
qualname_list = context.call_function_name_qual.split(".")
func = qualname_list[-1]
if "hashlib" in qualname_list:
return _hashlib_func(context, func)
elif "crypt" in qualname_list and func in ("crypt", "mksalt"):
return _crypt_crypt(context, func)

View File

@@ -0,0 +1,153 @@
# SPDX-License-Identifier: Apache-2.0
r"""
================================================
B615: Test for unsafe Hugging Face Hub downloads
================================================
This plugin checks for unsafe downloads from Hugging Face Hub without proper
integrity verification. Downloading models, datasets, or files without
specifying a revision based on an immmutable revision (commit) can
lead to supply chain attacks where malicious actors could
replace model files and use an existing tag or branch name
to serve malicious content.
The secure approach is to:
1. Pin to specific revisions/commits when downloading models, files or datasets
Common unsafe patterns:
- ``AutoModel.from_pretrained("org/model-name")``
- ``AutoModel.from_pretrained("org/model-name", revision="main")``
- ``AutoModel.from_pretrained("org/model-name", revision="v1.0.0")``
- ``load_dataset("org/dataset-name")`` without revision
- ``load_dataset("org/dataset-name", revision="main")``
- ``load_dataset("org/dataset-name", revision="v1.0")``
- ``AutoTokenizer.from_pretrained("org/model-name")``
- ``AutoTokenizer.from_pretrained("org/model-name", revision="main")``
- ``AutoTokenizer.from_pretrained("org/model-name", revision="v3.3.0")``
- ``hf_hub_download(repo_id="org/model_name", filename="file_name")``
- ``hf_hub_download(repo_id="org/model_name",
filename="file_name",
revision="main"
)``
- ``hf_hub_download(repo_id="org/model_name",
filename="file_name",
revision="v2.0.0"
)``
- ``snapshot_download(repo_id="org/model_name")``
- ``snapshot_download(repo_id="org/model_name", revision="main")``
- ``snapshot_download(repo_id="org/model_name", revision="refs/pr/1")``
:Example:
.. code-block:: none
>> Issue: Unsafe Hugging Face Hub download without revision pinning
Severity: Medium Confidence: High
CWE: CWE-494 (https://cwe.mitre.org/data/definitions/494.html)
Location: examples/huggingface_unsafe_download.py:8
7 # Unsafe: no revision specified
8 model = AutoModel.from_pretrained("org/model_name")
9
.. seealso::
- https://cwe.mitre.org/data/definitions/494.html
- https://huggingface.co/docs/huggingface_hub/en/guides/download
.. versionadded:: 1.8.6
"""
import string
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.checks("Call")
@test.test_id("B615")
def huggingface_unsafe_download(context):
"""
This plugin checks for unsafe artifact download from Hugging Face Hub
without immutable/reproducible revision pinning.
"""
# Check if any HuggingFace-related modules are imported
hf_modules = [
"transformers",
"datasets",
"huggingface_hub",
]
# Check if any HF modules are imported
hf_imported = any(
context.is_module_imported_like(module) for module in hf_modules
)
if not hf_imported:
return
qualname = context.call_function_name_qual
if not isinstance(qualname, str):
return
unsafe_patterns = {
# transformers library patterns
"from_pretrained": ["transformers"],
# datasets library patterns
"load_dataset": ["datasets"],
# huggingface_hub patterns
"hf_hub_download": ["huggingface_hub"],
"snapshot_download": ["huggingface_hub"],
"repository_id": ["huggingface_hub"],
}
qualname_parts = qualname.split(".")
func_name = qualname_parts[-1]
if func_name not in unsafe_patterns:
return
required_modules = unsafe_patterns[func_name]
if not any(module in qualname_parts for module in required_modules):
return
# Check for revision parameter (the key security control)
revision_value = context.get_call_arg_value("revision")
commit_id_value = context.get_call_arg_value("commit_id")
# Check if a revision or commit_id is specified
revision_to_check = revision_value or commit_id_value
if revision_to_check is not None:
# Check if it's a secure revision (looks like a commit hash)
# Commit hashes: 40 chars (full SHA) or 7+ chars (short SHA)
if isinstance(revision_to_check, str):
# Remove quotes if present
revision_str = str(revision_to_check).strip("\"'")
# Check if it looks like a commit hash (hexadecimal string)
# Must be at least 7 characters and all hexadecimal
is_hex = all(c in string.hexdigits for c in revision_str)
if len(revision_str) >= 7 and is_hex:
# This looks like a commit hash, which is secure
return
# Edge case: check if this is a local path (starts with ./ or /)
first_arg = context.get_call_arg_at_position(0)
if first_arg and isinstance(first_arg, str):
if first_arg.startswith(("./", "/", "../")):
# Local paths are generally safer
return
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
text=(
f"Unsafe Hugging Face Hub download without revision pinning "
f"in {func_name}()"
),
cwe=issue.Cwe.DOWNLOAD_OF_CODE_WITHOUT_INTEGRITY_CHECK,
lineno=context.get_lineno_for_call_arg(func_name),
)

View File

@@ -0,0 +1,63 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
==============================================
B601: Test for shell injection within Paramiko
==============================================
Paramiko is a Python library designed to work with the SSH2 protocol for secure
(encrypted and authenticated) connections to remote machines. It is intended to
run commands on a remote host. These commands are run within a shell on the
target and are thus vulnerable to various shell injection attacks. Bandit
reports a MEDIUM issue when it detects the use of Paramiko's "exec_command"
method advising the user to check inputs are correctly sanitized.
:Example:
.. code-block:: none
>> Issue: Possible shell injection via Paramiko call, check inputs are
properly sanitized.
Severity: Medium Confidence: Medium
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/paramiko_injection.py:4
3 # this is not safe
4 paramiko.exec_command('something; really; unsafe')
5
.. seealso::
- https://security.openstack.org
- https://github.com/paramiko/paramiko
- https://www.owasp.org/index.php/Command_Injection
- https://cwe.mitre.org/data/definitions/78.html
.. versionadded:: 0.12.0
.. versionchanged:: 1.7.3
CWE information added
"""
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.checks("Call")
@test.test_id("B601")
def paramiko_calls(context):
issue_text = (
"Possible shell injection via Paramiko call, check inputs "
"are properly sanitized."
)
for module in ["paramiko"]:
if context.is_module_imported_like(module):
if context.call_function_name in ["exec_command"]:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text=issue_text,
)

View File

@@ -0,0 +1,706 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
import ast
import re
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
# yuck, regex: starts with a windows drive letter (eg C:)
# or one of our path delimeter characters (/, \, .)
full_path_match = re.compile(r"^(?:[A-Za-z](?=\:)|[\\\/\.])")
def _evaluate_shell_call(context):
no_formatting = isinstance(
context.node.args[0], ast.Constant
) and isinstance(context.node.args[0].value, str)
if no_formatting:
return bandit.LOW
else:
return bandit.HIGH
def gen_config(name):
if name == "shell_injection":
return {
# Start a process using the subprocess module, or one of its
# wrappers.
"subprocess": [
"subprocess.Popen",
"subprocess.call",
"subprocess.check_call",
"subprocess.check_output",
"subprocess.run",
],
# Start a process with a function vulnerable to shell injection.
"shell": [
"os.system",
"os.popen",
"os.popen2",
"os.popen3",
"os.popen4",
"popen2.popen2",
"popen2.popen3",
"popen2.popen4",
"popen2.Popen3",
"popen2.Popen4",
"commands.getoutput",
"commands.getstatusoutput",
"subprocess.getoutput",
"subprocess.getstatusoutput",
],
# Start a process with a function that is not vulnerable to shell
# injection.
"no_shell": [
"os.execl",
"os.execle",
"os.execlp",
"os.execlpe",
"os.execv",
"os.execve",
"os.execvp",
"os.execvpe",
"os.spawnl",
"os.spawnle",
"os.spawnlp",
"os.spawnlpe",
"os.spawnv",
"os.spawnve",
"os.spawnvp",
"os.spawnvpe",
"os.startfile",
],
}
def has_shell(context):
keywords = context.node.keywords
result = False
if "shell" in context.call_keywords:
for key in keywords:
if key.arg == "shell":
val = key.value
if isinstance(val, ast.Constant) and (
isinstance(val.value, int)
or isinstance(val.value, float)
or isinstance(val.value, complex)
):
result = bool(val.value)
elif isinstance(val, ast.List):
result = bool(val.elts)
elif isinstance(val, ast.Dict):
result = bool(val.keys)
elif isinstance(val, ast.Name) and val.id in ["False", "None"]:
result = False
elif isinstance(val, ast.Constant):
result = val.value
else:
result = True
return result
@test.takes_config("shell_injection")
@test.checks("Call")
@test.test_id("B602")
def subprocess_popen_with_shell_equals_true(context, config):
"""**B602: Test for use of popen with shell equals true**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this test looks for the
spawning of a subprocess using a command shell. This type of subprocess
invocation is dangerous as it is vulnerable to various shell injection
attacks. Great care should be taken to sanitize all input in order to
mitigate this risk. Calls of this type are identified by a parameter of
'shell=True' being given.
Additionally, this plugin scans the command string given and adjusts its
reported severity based on how it is presented. If the command string is a
simple static string containing no special shell characters, then the
resulting issue has low severity. If the string is static, but contains
shell formatting characters or wildcards, then the reported issue is
medium. Finally, if the string is computed using Python's string
manipulation or formatting operations, then the reported issue has high
severity. These severity levels reflect the likelihood that the code is
vulnerable to injection.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_without_shell_equals_true`
- :doc:`../plugins/start_process_with_no_shell`
- :doc:`../plugins/start_process_with_a_shell`
- :doc:`../plugins/start_process_with_partial_path`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This plugin specifically scans for methods listed in `subprocess` section
that have shell=True specified.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its
wrappers.
subprocess:
- subprocess.Popen
- subprocess.call
:Example:
.. code-block:: none
>> Issue: subprocess call with shell=True seems safe, but may be
changed in the future, consider rewriting without shell
Severity: Low Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/subprocess_shell.py:21
20 subprocess.check_call(['/bin/ls', '-l'], shell=False)
21 subprocess.check_call('/bin/ls -l', shell=True)
22
>> Issue: call with shell=True contains special shell characters,
consider moving extra logic into Python code
Severity: Medium Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/subprocess_shell.py:26
25
26 subprocess.Popen('/bin/ls *', shell=True)
27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True)
>> Issue: subprocess call with shell=True identified, security issue.
Severity: High Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/subprocess_shell.py:27
26 subprocess.Popen('/bin/ls *', shell=True)
27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True)
28 subprocess.Popen('/bin/ls {}'.format('something'), shell=True)
.. seealso::
- https://security.openstack.org
- https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- https://security.openstack.org/guidelines/dg_avoid-shell-true.html
- https://cwe.mitre.org/data/definitions/78.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
if config and context.call_function_name_qual in config["subprocess"]:
if has_shell(context):
if len(context.call_args) > 0:
sev = _evaluate_shell_call(context)
if sev == bandit.LOW:
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text="subprocess call with shell=True seems safe, but "
"may be changed in the future, consider "
"rewriting without shell",
lineno=context.get_lineno_for_call_arg("shell"),
)
else:
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text="subprocess call with shell=True identified, "
"security issue.",
lineno=context.get_lineno_for_call_arg("shell"),
)
@test.takes_config("shell_injection")
@test.checks("Call")
@test.test_id("B603")
def subprocess_without_shell_equals_true(context, config):
"""**B603: Test for use of subprocess without shell equals true**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this test looks for the
spawning of a subprocess without the use of a command shell. This type of
subprocess invocation is not vulnerable to shell injection attacks, but
care should still be taken to ensure validity of input.
Because this is a lesser issue than that described in
`subprocess_popen_with_shell_equals_true` a LOW severity warning is
reported.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
- :doc:`../plugins/start_process_with_no_shell`
- :doc:`../plugins/start_process_with_a_shell`
- :doc:`../plugins/start_process_with_partial_path`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This plugin specifically scans for methods listed in `subprocess` section
that have shell=False specified.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its
wrappers.
subprocess:
- subprocess.Popen
- subprocess.call
:Example:
.. code-block:: none
>> Issue: subprocess call - check for execution of untrusted input.
Severity: Low Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/subprocess_shell.py:23
22
23 subprocess.check_output(['/bin/ls', '-l'])
24
.. seealso::
- https://security.openstack.org
- https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
- https://security.openstack.org/guidelines/dg_avoid-shell-true.html
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- https://cwe.mitre.org/data/definitions/78.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
if config and context.call_function_name_qual in config["subprocess"]:
if not has_shell(context):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text="subprocess call - check for execution of untrusted "
"input.",
lineno=context.get_lineno_for_call_arg("shell"),
)
@test.takes_config("shell_injection")
@test.checks("Call")
@test.test_id("B604")
def any_other_function_with_shell_equals_true(context, config):
"""**B604: Test for any function with shell equals true**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this plugin test
interrogates method calls for the presence of a keyword parameter `shell`
equalling true. It is related to detection of shell injection issues and is
intended to catch custom wrappers to vulnerable methods that may have been
created.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
- :doc:`../plugins/subprocess_without_shell_equals_true`
- :doc:`../plugins/start_process_with_no_shell`
- :doc:`../plugins/start_process_with_a_shell`
- :doc:`../plugins/start_process_with_partial_path`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
Specifically, this plugin excludes those functions listed under the
subprocess section, these methods are tested in a separate specific test
plugin and this exclusion prevents duplicate issue reporting.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its
wrappers.
subprocess: [subprocess.Popen, subprocess.call,
subprocess.check_call, subprocess.check_output
execute_with_timeout]
:Example:
.. code-block:: none
>> Issue: Function call with shell=True parameter identified, possible
security issue.
Severity: Medium Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/subprocess_shell.py:9
8 pop('/bin/gcc --version', shell=True)
9 Popen('/bin/gcc --version', shell=True)
10
.. seealso::
- https://security.openstack.org/guidelines/dg_avoid-shell-true.html
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- https://cwe.mitre.org/data/definitions/78.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
if config and context.call_function_name_qual not in config["subprocess"]:
if has_shell(context):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.LOW,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text="Function call with shell=True parameter identified, "
"possible security issue.",
lineno=context.get_lineno_for_call_arg("shell"),
)
@test.takes_config("shell_injection")
@test.checks("Call")
@test.test_id("B605")
def start_process_with_a_shell(context, config):
"""**B605: Test for starting a process with a shell**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this test looks for the
spawning of a subprocess using a command shell. This type of subprocess
invocation is dangerous as it is vulnerable to various shell injection
attacks. Great care should be taken to sanitize all input in order to
mitigate this risk. Calls of this type are identified by the use of certain
commands which are known to use shells. Bandit will report a LOW
severity warning.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_without_shell_equals_true`
- :doc:`../plugins/start_process_with_no_shell`
- :doc:`../plugins/start_process_with_partial_path`
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This plugin specifically scans for methods listed in `shell` section.
.. code-block:: yaml
shell_injection:
shell:
- os.system
- os.popen
- os.popen2
- os.popen3
- os.popen4
- popen2.popen2
- popen2.popen3
- popen2.popen4
- popen2.Popen3
- popen2.Popen4
- commands.getoutput
- commands.getstatusoutput
- subprocess.getoutput
- subprocess.getstatusoutput
:Example:
.. code-block:: none
>> Issue: Starting a process with a shell: check for injection.
Severity: Low Confidence: Medium
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: examples/os_system.py:3
2
3 os.system('/bin/echo hi')
.. seealso::
- https://security.openstack.org
- https://docs.python.org/3/library/os.html#os.system
- https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- https://cwe.mitre.org/data/definitions/78.html
.. versionadded:: 0.10.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
if config and context.call_function_name_qual in config["shell"]:
if len(context.call_args) > 0:
sev = _evaluate_shell_call(context)
if sev == bandit.LOW:
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text="Starting a process with a shell: "
"Seems safe, but may be changed in the future, "
"consider rewriting without shell",
)
else:
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text="Starting a process with a shell, possible injection"
" detected, security issue.",
)
@test.takes_config("shell_injection")
@test.checks("Call")
@test.test_id("B606")
def start_process_with_no_shell(context, config):
"""**B606: Test for starting a process with no shell**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this test looks for the
spawning of a subprocess in a way that doesn't use a shell. Although this
is generally safe, it maybe useful for penetration testing workflows to
track where external system calls are used. As such a LOW severity message
is generated.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_without_shell_equals_true`
- :doc:`../plugins/start_process_with_a_shell`
- :doc:`../plugins/start_process_with_partial_path`
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This plugin specifically scans for methods listed in `no_shell` section.
.. code-block:: yaml
shell_injection:
no_shell:
- os.execl
- os.execle
- os.execlp
- os.execlpe
- os.execv
- os.execve
- os.execvp
- os.execvpe
- os.spawnl
- os.spawnle
- os.spawnlp
- os.spawnlpe
- os.spawnv
- os.spawnve
- os.spawnvp
- os.spawnvpe
- os.startfile
:Example:
.. code-block:: none
>> Issue: [start_process_with_no_shell] Starting a process without a
shell.
Severity: Low Confidence: Medium
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: examples/os-spawn.py:8
7 os.spawnv(mode, path, args)
8 os.spawnve(mode, path, args, env)
9 os.spawnvp(mode, file, args)
.. seealso::
- https://security.openstack.org
- https://docs.python.org/3/library/os.html#os.system
- https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- https://cwe.mitre.org/data/definitions/78.html
.. versionadded:: 0.10.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
if config and context.call_function_name_qual in config["no_shell"]:
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text="Starting a process without a shell.",
)
@test.takes_config("shell_injection")
@test.checks("Call")
@test.test_id("B607")
def start_process_with_partial_path(context, config):
"""**B607: Test for starting a process with a partial path**
Python possesses many mechanisms to invoke an external executable. If the
desired executable path is not fully qualified relative to the filesystem
root then this may present a potential security risk.
In POSIX environments, the `PATH` environment variable is used to specify a
set of standard locations that will be searched for the first matching
named executable. While convenient, this behavior may allow a malicious
actor to exert control over a system. If they are able to adjust the
contents of the `PATH` variable, or manipulate the file system, then a
bogus executable may be discovered in place of the desired one. This
executable will be invoked with the user privileges of the Python process
that spawned it, potentially a highly privileged user.
This test will scan the parameters of all configured Python methods,
looking for paths that do not start at the filesystem root, that is, do not
have a leading '/' character.
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This test will scan parameters of all methods in all sections. Note that
methods are fully qualified and de-aliased prior to checking.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its
wrappers.
subprocess:
- subprocess.Popen
- subprocess.call
# Start a process with a function vulnerable to shell injection.
shell:
- os.system
- os.popen
- popen2.Popen3
- popen2.Popen4
- commands.getoutput
- commands.getstatusoutput
# Start a process with a function that is not vulnerable to shell
injection.
no_shell:
- os.execl
- os.execle
:Example:
.. code-block:: none
>> Issue: Starting a process with a partial executable path
Severity: Low Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/partial_path_process.py:3
2 from subprocess import Popen as pop
3 pop('gcc --version', shell=False)
.. seealso::
- https://security.openstack.org
- https://docs.python.org/3/library/os.html#process-management
- https://cwe.mitre.org/data/definitions/78.html
.. versionadded:: 0.13.0
.. versionchanged:: 1.7.3
CWE information added
"""
if config and len(context.call_args):
if (
context.call_function_name_qual in config["subprocess"]
or context.call_function_name_qual in config["shell"]
or context.call_function_name_qual in config["no_shell"]
):
node = context.node.args[0]
# some calls take an arg list, check the first part
if isinstance(node, ast.List) and node.elts:
node = node.elts[0]
# make sure the param is a string literal and not a var name
if (
isinstance(node, ast.Constant)
and isinstance(node.value, str)
and not full_path_match.match(node.value)
):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
cwe=issue.Cwe.OS_COMMAND_INJECTION,
text="Starting a process with a partial executable path",
)

View File

@@ -0,0 +1,143 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
============================
B608: Test for SQL injection
============================
An SQL injection attack consists of insertion or "injection" of a SQL query via
the input data given to an application. It is a very common attack vector. This
plugin test looks for strings that resemble SQL statements that are involved in
some form of string building operation. For example:
- "SELECT %s FROM derp;" % var
- "SELECT thing FROM " + tab
- "SELECT " + val + " FROM " + tab + ...
- "SELECT {} FROM derp;".format(var)
- f"SELECT foo FROM bar WHERE id = {product}"
Unless care is taken to sanitize and control the input data when building such
SQL statement strings, an injection attack becomes possible. If strings of this
nature are discovered, a LOW confidence issue is reported. In order to boost
result confidence, this plugin test will also check to see if the discovered
string is in use with standard Python DBAPI calls `execute` or `executemany`.
If so, a MEDIUM issue is reported. For example:
- cursor.execute("SELECT %s FROM derp;" % var)
Use of str.replace in the string construction can also be dangerous.
For example:
- "SELECT * FROM foo WHERE id = '[VALUE]'".replace("[VALUE]", identifier)
However, such cases are always reported with LOW confidence to compensate
for false positives, since valid uses of str.replace can be common.
:Example:
.. code-block:: none
>> Issue: Possible SQL injection vector through string-based query
construction.
Severity: Medium Confidence: Low
CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
Location: ./examples/sql_statements.py:4
3 query = "DELETE FROM foo WHERE id = '%s'" % identifier
4 query = "UPDATE foo SET value = 'b' WHERE id = '%s'" % identifier
5
.. seealso::
- https://www.owasp.org/index.php/SQL_Injection
- https://security.openstack.org/guidelines/dg_parameterize-database-queries.html
- https://cwe.mitre.org/data/definitions/89.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
.. versionchanged:: 1.7.7
Flag when str.replace is used in the string construction
""" # noqa: E501
import ast
import re
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
from bandit.core import utils
SIMPLE_SQL_RE = re.compile(
r"(select\s.*from\s|"
r"delete\s+from\s|"
r"insert\s+into\s.*values\s|"
r"update\s.*set\s)",
re.IGNORECASE | re.DOTALL,
)
def _check_string(data):
return SIMPLE_SQL_RE.search(data) is not None
def _evaluate_ast(node):
wrapper = None
statement = ""
str_replace = False
if isinstance(node._bandit_parent, ast.BinOp):
out = utils.concat_string(node, node._bandit_parent)
wrapper = out[0]._bandit_parent
statement = out[1]
elif isinstance(
node._bandit_parent, ast.Attribute
) and node._bandit_parent.attr in ("format", "replace"):
statement = node.value
# Hierarchy for "".format() is Wrapper -> Call -> Attribute -> Str
wrapper = node._bandit_parent._bandit_parent._bandit_parent
if node._bandit_parent.attr == "replace":
str_replace = True
elif hasattr(ast, "JoinedStr") and isinstance(
node._bandit_parent, ast.JoinedStr
):
substrings = [
child
for child in node._bandit_parent.values
if isinstance(child, ast.Constant) and isinstance(child.value, str)
]
# JoinedStr consists of list of Constant and FormattedValue
# instances. Let's perform one test for the whole string
# and abandon all parts except the first one to raise one
# failed test instead of many for the same SQL statement.
if substrings and node == substrings[0]:
statement = "".join([str(child.value) for child in substrings])
wrapper = node._bandit_parent._bandit_parent
if isinstance(wrapper, ast.Call): # wrapped in "execute" call?
names = ["execute", "executemany"]
name = utils.get_called_name(wrapper)
return (name in names, statement, str_replace)
else:
return (False, statement, str_replace)
@test.checks("Str")
@test.test_id("B608")
def hardcoded_sql_expressions(context):
execute_call, statement, str_replace = _evaluate_ast(context.node)
if _check_string(statement):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=(
bandit.MEDIUM
if execute_call and not str_replace
else bandit.LOW
),
cwe=issue.Cwe.SQL_INJECTION,
text="Possible SQL injection vector through string-based "
"query construction.",
)

View File

@@ -0,0 +1,144 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
========================================
B609: Test for use of wildcard injection
========================================
Python provides a number of methods that emulate the behavior of standard Linux
command line utilities. Like their Linux counterparts, these commands may take
a wildcard "\*" character in place of a file system path. This is interpreted
to mean "any and all files or folders" and can be used to build partially
qualified paths, such as "/home/user/\*".
The use of partially qualified paths may result in unintended consequences if
an unexpected file or symlink is placed into the path location given. This
becomes particularly dangerous when combined with commands used to manipulate
file permissions or copy data off of a system.
This test plugin looks for usage of the following commands in conjunction with
wild card parameters:
- 'chown'
- 'chmod'
- 'tar'
- 'rsync'
As well as any method configured in the shell or subprocess injection test
configurations.
**Config Options:**
This plugin test shares a configuration with others in the same family, namely
`shell_injection`. This configuration is divided up into three sections,
`subprocess`, `shell` and `no_shell`. They each list Python calls that spawn
subprocesses, invoke commands within a shell, or invoke commands without a
shell (by replacing the calling process) respectively.
This test will scan parameters of all methods in all sections. Note that
methods are fully qualified and de-aliased prior to checking.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its wrappers.
subprocess:
- subprocess.Popen
- subprocess.call
# Start a process with a function vulnerable to shell injection.
shell:
- os.system
- os.popen
- popen2.Popen3
- popen2.Popen4
- commands.getoutput
- commands.getstatusoutput
# Start a process with a function that is not vulnerable to shell
injection.
no_shell:
- os.execl
- os.execle
:Example:
.. code-block:: none
>> Issue: Possible wildcard injection in call: subprocess.Popen
Severity: High Confidence: Medium
CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/wildcard-injection.py:8
7 o.popen2('/bin/chmod *')
8 subp.Popen('/bin/chown *', shell=True)
9
>> Issue: subprocess call - check for execution of untrusted input.
Severity: Low Confidence: High
CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./examples/wildcard-injection.py:11
10 # Not vulnerable to wildcard injection
11 subp.Popen('/bin/rsync *')
12 subp.Popen("/bin/chmod *")
.. seealso::
- https://security.openstack.org
- https://en.wikipedia.org/wiki/Wildcard_character
- https://www.defensecode.com/public/DefenseCode_Unix_WildCards_Gone_Wild.txt
- https://cwe.mitre.org/data/definitions/78.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
"""
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
from bandit.plugins import injection_shell # NOTE(tkelsey): shared config
gen_config = injection_shell.gen_config
@test.takes_config("shell_injection")
@test.checks("Call")
@test.test_id("B609")
def linux_commands_wildcard_injection(context, config):
if not ("shell" in config and "subprocess" in config):
return
vulnerable_funcs = ["chown", "chmod", "tar", "rsync"]
if context.call_function_name_qual in config["shell"] or (
context.call_function_name_qual in config["subprocess"]
and context.check_call_arg_value("shell", "True")
):
if context.call_args_count >= 1:
call_argument = context.get_call_arg_at_position(0)
argument_string = ""
if isinstance(call_argument, list):
for li in call_argument:
argument_string += f" {li}"
elif isinstance(call_argument, str):
argument_string = call_argument
if argument_string != "":
for vulnerable_func in vulnerable_funcs:
if (
vulnerable_func in argument_string
and "*" in argument_string
):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.IMPROPER_WILDCARD_NEUTRALIZATION,
text="Possible wildcard injection in call: %s"
% context.call_function_name_qual,
lineno=context.get_lineno_for_call_arg("shell"),
)

View File

@@ -0,0 +1,285 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def get_bad_proto_versions(config):
return config["bad_protocol_versions"]
def gen_config(name):
if name == "ssl_with_bad_version":
return {
"bad_protocol_versions": [
"PROTOCOL_SSLv2",
"SSLv2_METHOD",
"SSLv23_METHOD",
"PROTOCOL_SSLv3", # strict option
"PROTOCOL_TLSv1", # strict option
"SSLv3_METHOD", # strict option
"TLSv1_METHOD",
"PROTOCOL_TLSv1_1",
"TLSv1_1_METHOD",
]
} # strict option
@test.takes_config
@test.checks("Call")
@test.test_id("B502")
def ssl_with_bad_version(context, config):
"""**B502: Test for SSL use with bad version used**
Several highly publicized exploitable flaws have been discovered
in all versions of SSL and early versions of TLS. It is strongly
recommended that use of the following known broken protocol versions be
avoided:
- SSL v2
- SSL v3
- TLS v1
- TLS v1.1
This plugin test scans for calls to Python methods with parameters that
indicate the used broken SSL/TLS protocol versions. Currently, detection
supports methods using Python's native SSL/TLS support and the pyOpenSSL
module. A HIGH severity warning will be reported whenever known broken
protocol versions are detected.
It is worth noting that native support for TLS 1.2 is only available in
more recent Python versions, specifically 2.7.9 and up, and 3.x
A note on 'SSLv23':
Amongst the available SSL/TLS versions provided by Python/pyOpenSSL there
exists the option to use SSLv23. This very poorly named option actually
means "use the highest version of SSL/TLS supported by both the server and
client". This may (and should be) a version well in advance of SSL v2 or
v3. Bandit can scan for the use of SSLv23 if desired, but its detection
does not necessarily indicate a problem.
When using SSLv23 it is important to also provide flags to explicitly
exclude bad versions of SSL/TLS from the protocol versions considered. Both
the Python native and pyOpenSSL modules provide the ``OP_NO_SSLv2`` and
``OP_NO_SSLv3`` flags for this purpose.
**Config Options:**
.. code-block:: yaml
ssl_with_bad_version:
bad_protocol_versions:
- PROTOCOL_SSLv2
- SSLv2_METHOD
- SSLv23_METHOD
- PROTOCOL_SSLv3 # strict option
- PROTOCOL_TLSv1 # strict option
- SSLv3_METHOD # strict option
- TLSv1_METHOD # strict option
:Example:
.. code-block:: none
>> Issue: ssl.wrap_socket call with insecure SSL/TLS protocol version
identified, security issue.
Severity: High Confidence: High
CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html)
Location: ./examples/ssl-insecure-version.py:13
12 # strict tests
13 ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3)
14 ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1)
.. seealso::
- :func:`ssl_with_bad_defaults`
- :func:`ssl_with_no_version`
- https://heartbleed.com/
- https://en.wikipedia.org/wiki/POODLE
- https://security.openstack.org/guidelines/dg_move-data-securely.html
- https://cwe.mitre.org/data/definitions/327.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
.. versionchanged:: 1.7.5
Added TLS 1.1
"""
bad_ssl_versions = get_bad_proto_versions(config)
if context.call_function_name_qual == "ssl.wrap_socket":
if context.check_call_arg_value("ssl_version", bad_ssl_versions):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
cwe=issue.Cwe.BROKEN_CRYPTO,
text="ssl.wrap_socket call with insecure SSL/TLS protocol "
"version identified, security issue.",
lineno=context.get_lineno_for_call_arg("ssl_version"),
)
elif context.call_function_name_qual == "pyOpenSSL.SSL.Context":
if context.check_call_arg_value("method", bad_ssl_versions):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
cwe=issue.Cwe.BROKEN_CRYPTO,
text="SSL.Context call with insecure SSL/TLS protocol "
"version identified, security issue.",
lineno=context.get_lineno_for_call_arg("method"),
)
elif (
context.call_function_name_qual != "ssl.wrap_socket"
and context.call_function_name_qual != "pyOpenSSL.SSL.Context"
):
if context.check_call_arg_value(
"method", bad_ssl_versions
) or context.check_call_arg_value("ssl_version", bad_ssl_versions):
lineno = context.get_lineno_for_call_arg(
"method"
) or context.get_lineno_for_call_arg("ssl_version")
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.BROKEN_CRYPTO,
text="Function call with insecure SSL/TLS protocol "
"identified, possible security issue.",
lineno=lineno,
)
@test.takes_config("ssl_with_bad_version")
@test.checks("FunctionDef")
@test.test_id("B503")
def ssl_with_bad_defaults(context, config):
"""**B503: Test for SSL use with bad defaults specified**
This plugin is part of a family of tests that detect the use of known bad
versions of SSL/TLS, please see :doc:`../plugins/ssl_with_bad_version` for
a complete discussion. Specifically, this plugin test scans for Python
methods with default parameter values that specify the use of broken
SSL/TLS protocol versions. Currently, detection supports methods using
Python's native SSL/TLS support and the pyOpenSSL module. A MEDIUM severity
warning will be reported whenever known broken protocol versions are
detected.
**Config Options:**
This test shares the configuration provided for the standard
:doc:`../plugins/ssl_with_bad_version` test, please refer to its
documentation.
:Example:
.. code-block:: none
>> Issue: Function definition identified with insecure SSL/TLS protocol
version by default, possible security issue.
Severity: Medium Confidence: Medium
CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html)
Location: ./examples/ssl-insecure-version.py:28
27
28 def open_ssl_socket(version=SSL.SSLv2_METHOD):
29 pass
.. seealso::
- :func:`ssl_with_bad_version`
- :func:`ssl_with_no_version`
- https://heartbleed.com/
- https://en.wikipedia.org/wiki/POODLE
- https://security.openstack.org/guidelines/dg_move-data-securely.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
.. versionchanged:: 1.7.5
Added TLS 1.1
"""
bad_ssl_versions = get_bad_proto_versions(config)
for default in context.function_def_defaults_qual:
val = default.split(".")[-1]
if val in bad_ssl_versions:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.BROKEN_CRYPTO,
text="Function definition identified with insecure SSL/TLS "
"protocol version by default, possible security "
"issue.",
)
@test.checks("Call")
@test.test_id("B504")
def ssl_with_no_version(context):
"""**B504: Test for SSL use with no version specified**
This plugin is part of a family of tests that detect the use of known bad
versions of SSL/TLS, please see :doc:`../plugins/ssl_with_bad_version` for
a complete discussion. Specifically, This plugin test scans for specific
methods in Python's native SSL/TLS support and the pyOpenSSL module that
configure the version of SSL/TLS protocol to use. These methods are known
to provide default value that maximize compatibility, but permit use of the
aforementioned broken protocol versions. A LOW severity warning will be
reported whenever this is detected.
**Config Options:**
This test shares the configuration provided for the standard
:doc:`../plugins/ssl_with_bad_version` test, please refer to its
documentation.
:Example:
.. code-block:: none
>> Issue: ssl.wrap_socket call with no SSL/TLS protocol version
specified, the default SSLv23 could be insecure, possible security
issue.
Severity: Low Confidence: Medium
CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html)
Location: ./examples/ssl-insecure-version.py:23
22
23 ssl.wrap_socket()
24
.. seealso::
- :func:`ssl_with_bad_version`
- :func:`ssl_with_bad_defaults`
- https://heartbleed.com/
- https://en.wikipedia.org/wiki/POODLE
- https://security.openstack.org/guidelines/dg_move-data-securely.html
.. versionadded:: 0.9.0
.. versionchanged:: 1.7.3
CWE information added
"""
if context.call_function_name_qual == "ssl.wrap_socket":
if context.check_call_arg_value("ssl_version") is None:
# check_call_arg_value() returns False if the argument is found
# but does not match the supplied value (or the default None).
# It returns None if the arg_name passed doesn't exist. This
# tests for that (ssl_version is not specified).
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.BROKEN_CRYPTO,
text="ssl.wrap_socket call with no SSL/TLS protocol version "
"specified, the default SSLv23 could be insecure, "
"possible security issue.",
lineno=context.get_lineno_for_call_arg("ssl_version"),
)

View File

@@ -0,0 +1,134 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
==========================================
B701: Test for not auto escaping in jinja2
==========================================
Jinja2 is a Python HTML templating system. It is typically used to build web
applications, though appears in other places well, notably the Ansible
automation system. When configuring the Jinja2 environment, the option to use
autoescaping on input can be specified. When autoescaping is enabled, Jinja2
will filter input strings to escape any HTML content submitted via template
variables. Without escaping HTML input the application becomes vulnerable to
Cross Site Scripting (XSS) attacks.
Unfortunately, autoescaping is False by default. Thus this plugin test will
warn on omission of an autoescape setting, as well as an explicit setting of
false. A HIGH severity warning is generated in either of these scenarios.
:Example:
.. code-block:: none
>> Issue: Using jinja2 templates with autoescape=False is dangerous and can
lead to XSS. Use autoescape=True to mitigate XSS vulnerabilities.
Severity: High Confidence: High
CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html)
Location: ./examples/jinja2_templating.py:11
10 templateEnv = jinja2.Environment(autoescape=False,
loader=templateLoader)
11 Environment(loader=templateLoader,
12 load=templateLoader,
13 autoescape=False)
14
>> Issue: By default, jinja2 sets autoescape to False. Consider using
autoescape=True or use the select_autoescape function to mitigate XSS
vulnerabilities.
Severity: High Confidence: High
CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html)
Location: ./examples/jinja2_templating.py:15
14
15 Environment(loader=templateLoader,
16 load=templateLoader)
17
18 Environment(autoescape=select_autoescape(['html', 'htm', 'xml']),
19 loader=templateLoader)
.. seealso::
- `OWASP XSS <https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)>`__
- https://realpython.com/primer-on-jinja-templating/
- https://jinja.palletsprojects.com/en/2.11.x/api/#autoescaping
- https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html
- https://cwe.mitre.org/data/definitions/94.html
.. versionadded:: 0.10.0
.. versionchanged:: 1.7.3
CWE information added
"""
import ast
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.checks("Call")
@test.test_id("B701")
def jinja2_autoescape_false(context):
# check type just to be safe
if isinstance(context.call_function_name_qual, str):
qualname_list = context.call_function_name_qual.split(".")
func = qualname_list[-1]
if "jinja2" in qualname_list and func == "Environment":
for node in ast.walk(context.node):
if isinstance(node, ast.keyword):
# definite autoescape = False
if getattr(node, "arg", None) == "autoescape" and (
getattr(node.value, "id", None) == "False"
or getattr(node.value, "value", None) is False
):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
cwe=issue.Cwe.CODE_INJECTION,
text="Using jinja2 templates with autoescape="
"False is dangerous and can lead to XSS. "
"Use autoescape=True or use the "
"select_autoescape function to mitigate XSS "
"vulnerabilities.",
)
# found autoescape
if getattr(node, "arg", None) == "autoescape":
value = getattr(node, "value", None)
if (
getattr(value, "id", None) == "True"
or getattr(value, "value", None) is True
):
return
# Check if select_autoescape function is used.
elif isinstance(value, ast.Call) and (
getattr(value.func, "attr", None)
== "select_autoescape"
or getattr(value.func, "id", None)
== "select_autoescape"
):
return
else:
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.CODE_INJECTION,
text="Using jinja2 templates with autoescape="
"False is dangerous and can lead to XSS. "
"Ensure autoescape=True or use the "
"select_autoescape function to mitigate "
"XSS vulnerabilities.",
)
# We haven't found a keyword named autoescape, indicating default
# behavior
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
cwe=issue.Cwe.CODE_INJECTION,
text="By default, jinja2 sets autoescape to False. Consider "
"using autoescape=True or use the select_autoescape "
"function to mitigate XSS vulnerabilities.",
)

View File

@@ -0,0 +1,58 @@
# Copyright (c) 2022 Rajesh Pangare
#
# SPDX-License-Identifier: Apache-2.0
r"""
====================================================
B612: Test for insecure use of logging.config.listen
====================================================
This plugin test checks for the unsafe usage of the
``logging.config.listen`` function. The logging.config.listen
function provides the ability to listen for external
configuration files on a socket server. Because portions of the
configuration are passed through eval(), use of this function
may open its users to a security risk. While the function only
binds to a socket on localhost, and so does not accept connections
from remote machines, there are scenarios where untrusted code
could be run under the account of the process which calls listen().
logging.config.listen provides the ability to verify bytes received
across the socket with signature verification or encryption/decryption.
:Example:
.. code-block:: none
>> Issue: [B612:logging_config_listen] Use of insecure
logging.config.listen detected.
Severity: Medium Confidence: High
CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html)
Location: examples/logging_config_insecure_listen.py:3:4
2
3 t = logging.config.listen(9999)
.. seealso::
- https://docs.python.org/3/library/logging.config.html#logging.config.listen
.. versionadded:: 1.7.5
"""
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.checks("Call")
@test.test_id("B612")
def logging_config_insecure_listen(context):
if (
context.call_function_name_qual == "logging.config.listen"
and "verify" not in context.call_keywords
):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.CODE_INJECTION,
text="Use of insecure logging.config.listen detected.",
)

View File

@@ -0,0 +1,69 @@
#
# SPDX-License-Identifier: Apache-2.0
r"""
====================================
B702: Test for use of mako templates
====================================
Mako is a Python templating system often used to build web applications. It is
the default templating system used in Pylons and Pyramid. Unlike Jinja2 (an
alternative templating system), Mako has no environment wide variable escaping
mechanism. Because of this, all input variables must be carefully escaped
before use to prevent possible vulnerabilities to Cross Site Scripting (XSS)
attacks.
:Example:
.. code-block:: none
>> Issue: Mako templates allow HTML/JS rendering by default and are
inherently open to XSS attacks. Ensure variables in all templates are
properly sanitized via the 'n', 'h' or 'x' flags (depending on context).
For example, to HTML escape the variable 'data' do ${ data |h }.
Severity: Medium Confidence: High
CWE: CWE-80 (https://cwe.mitre.org/data/definitions/80.html)
Location: ./examples/mako_templating.py:10
9
10 mako.template.Template("hern")
11 template.Template("hern")
.. seealso::
- https://www.makotemplates.org/
- `OWASP XSS <https://owasp.org/www-community/attacks/xss/>`__
- https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html
- https://cwe.mitre.org/data/definitions/80.html
.. versionadded:: 0.10.0
.. versionchanged:: 1.7.3
CWE information added
"""
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.checks("Call")
@test.test_id("B702")
def use_of_mako_templates(context):
# check type just to be safe
if isinstance(context.call_function_name_qual, str):
qualname_list = context.call_function_name_qual.split(".")
func = qualname_list[-1]
if "mako" in qualname_list and func == "Template":
# unlike Jinja2, mako does not have a template wide autoescape
# feature and thus each variable must be carefully sanitized.
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.BASIC_XSS,
text="Mako templates allow HTML/JS rendering by default and "
"are inherently open to XSS attacks. Ensure variables "
"in all templates are properly sanitized via the 'n', "
"'h' or 'x' flags (depending on context). For example, "
"to HTML escape the variable 'data' do ${ data |h }.",
)

View File

@@ -0,0 +1,118 @@
# Copyright (c) 2025 David Salvisberg
#
# SPDX-License-Identifier: Apache-2.0
r"""
============================================
B704: Potential XSS on markupsafe.Markup use
============================================
``markupsafe.Markup`` does not perform any escaping, so passing dynamic
content, like f-strings, variables or interpolated strings will potentially
lead to XSS vulnerabilities, especially if that data was submitted by users.
Instead you should interpolate the resulting ``markupsafe.Markup`` object,
which will perform escaping, or use ``markupsafe.escape``.
**Config Options:**
This plugin allows you to specify additional callable that should be treated
like ``markupsafe.Markup``. By default we recognize ``flask.Markup`` as
an alias, but there are other subclasses or similar classes in the wild
that you may wish to treat the same.
Additionally there is a whitelist for callable names, whose result may
be safely passed into ``markupsafe.Markup``. This is useful for escape
functions like e.g. ``bleach.clean`` which don't themselves return
``markupsafe.Markup``, so they need to be wrapped. Take care when using
this setting, since incorrect use may introduce false negatives.
These two options can be set in a shared configuration section
`markupsafe_xss`.
.. code-block:: yaml
markupsafe_xss:
# Recognize additional aliases
extend_markup_names:
- webhelpers.html.literal
- my_package.Markup
# Allow the output of these functions to pass into Markup
allowed_calls:
- bleach.clean
- my_package.sanitize
:Example:
.. code-block:: none
>> Issue: [B704:markupsafe_markup_xss] Potential XSS with
``markupsafe.Markup`` detected. Do not use ``Markup``
on untrusted data.
Severity: Medium Confidence: High
CWE: CWE-79 (https://cwe.mitre.org/data/definitions/79.html)
Location: ./examples/markupsafe_markup_xss.py:5:0
4 content = "<script>alert('Hello, world!')</script>"
5 Markup(f"unsafe {content}")
6 flask.Markup("unsafe {}".format(content))
.. seealso::
- https://pypi.org/project/MarkupSafe/
- https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup
- https://cwe.mitre.org/data/definitions/79.html
.. versionadded:: 1.8.3
"""
import ast
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
from bandit.core.utils import get_call_name
def gen_config(name):
if name == "markupsafe_xss":
return {
"extend_markup_names": [],
"allowed_calls": [],
}
@test.takes_config("markupsafe_xss")
@test.checks("Call")
@test.test_id("B704")
def markupsafe_markup_xss(context, config):
qualname = context.call_function_name_qual
if qualname not in ("markupsafe.Markup", "flask.Markup"):
if qualname not in config.get("extend_markup_names", []):
# not a Markup call
return None
args = context.node.args
if not args or isinstance(args[0], ast.Constant):
# both no arguments and a constant are fine
return None
allowed_calls = config.get("allowed_calls", [])
if (
allowed_calls
and isinstance(args[0], ast.Call)
and get_call_name(args[0], context.import_aliases) in allowed_calls
):
# the argument contains a whitelisted call
return None
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.XSS,
text=f"Potential XSS with ``{qualname}`` detected. Do "
f"not use ``{context.call_function_name}`` on untrusted data.",
)

View File

@@ -0,0 +1,81 @@
# Copyright (c) 2024 Stacklok, Inc.
#
# SPDX-License-Identifier: Apache-2.0
r"""
==================================
B614: Test for unsafe PyTorch load
==================================
This plugin checks for unsafe use of `torch.load`. Using `torch.load` with
untrusted data can lead to arbitrary code execution. There are two safe
alternatives:
1. Use `torch.load` with `weights_only=True` where only tensor data is
extracted, and no arbitrary Python objects are deserialized
2. Use the `safetensors` library from huggingface, which provides a safe
deserialization mechanism
With `weights_only=True`, PyTorch enforces a strict type check, ensuring
that only torch.Tensor objects are loaded.
:Example:
.. code-block:: none
>> Issue: Use of unsafe PyTorch load
Severity: Medium Confidence: High
CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html)
Location: examples/pytorch_load_save.py:8
7 loaded_model.load_state_dict(torch.load('model_weights.pth'))
8 another_model.load_state_dict(torch.load('model_weights.pth',
map_location='cpu'))
9
10 print("Model loaded successfully!")
.. seealso::
- https://cwe.mitre.org/data/definitions/94.html
- https://pytorch.org/docs/stable/generated/torch.load.html#torch.load
- https://github.com/huggingface/safetensors
.. versionadded:: 1.7.10
"""
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.checks("Call")
@test.test_id("B614")
def pytorch_load(context):
"""
This plugin checks for unsafe use of `torch.load`. Using `torch.load`
with untrusted data can lead to arbitrary code execution. The safe
alternative is to use `weights_only=True` or the safetensors library.
"""
imported = context.is_module_imported_exact("torch")
qualname = context.call_function_name_qual
if not imported and isinstance(qualname, str):
return
qualname_list = qualname.split(".")
func = qualname_list[-1]
if all(
[
"torch" in qualname_list,
func == "load",
]
):
# For torch.load, check if weights_only=True is specified
weights_only = context.get_call_arg_value("weights_only")
if weights_only == "True" or weights_only is True:
return
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
text="Use of unsafe PyTorch load",
cwe=issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA,
lineno=context.get_lineno_for_call_arg("load"),
)

View File

@@ -0,0 +1,84 @@
# SPDX-License-Identifier: Apache-2.0
r"""
=======================================
B113: Test for missing requests timeout
=======================================
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.
When request methods are used without the timeout parameter set,
Bandit will return a MEDIUM severity error.
:Example:
.. code-block:: none
>> 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
Location: examples/requests-missing-timeout.py:3:0
2
3 requests.get('https://gmail.com')
4 requests.get('https://gmail.com', timeout=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
Location: examples/requests-missing-timeout.py:4:0
3 requests.get('https://gmail.com')
4 requests.get('https://gmail.com', timeout=None)
5 requests.get('https://gmail.com', timeout=5)
.. seealso::
- https://requests.readthedocs.io/en/latest/user/advanced/#timeouts
.. versionadded:: 1.7.5
.. versionchanged:: 1.7.10
Added check for httpx module
""" # noqa: E501
import bandit
from bandit.core import issue
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"}
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:
# 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=f"Call to {qualname} without timeout",
)
if (
qualname == "requests"
and context.call_function_name in HTTP_VERBS
or qualname == "httpx"
and context.call_function_name in HTTPX_ATTRS
):
# check for timeout=None
if context.check_call_arg_value("timeout", "None"):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.LOW,
cwe=issue.Cwe.UNCONTROLLED_RESOURCE_CONSUMPTION,
text=f"Call to {qualname} with timeout set to None",
)

View File

@@ -0,0 +1,110 @@
#
# Copyright (c) 2018 SolarWinds, Inc.
#
# SPDX-License-Identifier: Apache-2.0
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.checks("Call")
@test.test_id("B508")
def snmp_insecure_version_check(context):
"""**B508: Checking for insecure SNMP versions**
This test is for checking for the usage of insecure SNMP version like
v1, v2c
Please update your code to use more secure versions of SNMP.
:Example:
.. code-block:: none
>> Issue: [B508:snmp_insecure_version_check] The use of SNMPv1 and
SNMPv2 is insecure. You should use SNMPv3 if able.
Severity: Medium Confidence: High
CWE: CWE-319 (https://cwe.mitre.org/data/definitions/319.html)
Location: examples/snmp.py:4:4
More Info: https://bandit.readthedocs.io/en/latest/plugins/b508_snmp_insecure_version_check.html
3 # SHOULD FAIL
4 a = CommunityData('public', mpModel=0)
5 # SHOULD FAIL
.. seealso::
- http://snmplabs.com/pysnmp/examples/hlapi/asyncore/sync/manager/cmdgen/snmp-versions.html
- https://cwe.mitre.org/data/definitions/319.html
.. versionadded:: 1.7.2
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
if context.call_function_name_qual == "pysnmp.hlapi.CommunityData":
# We called community data. Lets check our args
if context.check_call_arg_value(
"mpModel", 0
) or context.check_call_arg_value("mpModel", 1):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.CLEARTEXT_TRANSMISSION,
text="The use of SNMPv1 and SNMPv2 is insecure. "
"You should use SNMPv3 if able.",
lineno=context.get_lineno_for_call_arg("CommunityData"),
)
@test.checks("Call")
@test.test_id("B509")
def snmp_crypto_check(context):
"""**B509: Checking for weak cryptography**
This test is for checking for the usage of insecure SNMP cryptography:
v3 using noAuthNoPriv.
Please update your code to use more secure versions of SNMP. For example:
Instead of:
`CommunityData('public', mpModel=0)`
Use (Defaults to usmHMACMD5AuthProtocol and usmDESPrivProtocol
`UsmUserData("securityName", "authName", "privName")`
:Example:
.. code-block:: none
>> Issue: [B509:snmp_crypto_check] You should not use SNMPv3 without encryption. noAuthNoPriv & authNoPriv is insecure
Severity: Medium CWE: CWE-319 (https://cwe.mitre.org/data/definitions/319.html) Confidence: High
Location: examples/snmp.py:6:11
More Info: https://bandit.readthedocs.io/en/latest/plugins/b509_snmp_crypto_check.html
5 # SHOULD FAIL
6 insecure = UsmUserData("securityName")
7 # SHOULD FAIL
.. seealso::
- http://snmplabs.com/pysnmp/examples/hlapi/asyncore/sync/manager/cmdgen/snmp-versions.html
- https://cwe.mitre.org/data/definitions/319.html
.. versionadded:: 1.7.2
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
if context.call_function_name_qual == "pysnmp.hlapi.UsmUserData":
if context.call_args_count < 3:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.CLEARTEXT_TRANSMISSION,
text="You should not use SNMPv3 without encryption. "
"noAuthNoPriv & authNoPriv is insecure",
lineno=context.get_lineno_for_call_arg("UsmUserData"),
)

View File

@@ -0,0 +1,76 @@
# Copyright (c) 2018 VMware, Inc.
#
# SPDX-License-Identifier: Apache-2.0
r"""
==========================================
B507: Test for missing host key validation
==========================================
Encryption in general is typically critical to the security of many
applications. Using SSH can greatly increase security by guaranteeing the
identity of the party you are communicating with. This is accomplished by one
or both parties presenting trusted host keys during the connection
initialization phase of SSH.
When paramiko methods are used, host keys are verified by default. If host key
verification is disabled, Bandit will return a HIGH severity error.
:Example:
.. code-block:: none
>> Issue: [B507:ssh_no_host_key_verification] Paramiko call with policy set
to automatically trust the unknown host key.
Severity: High Confidence: Medium
CWE: CWE-295 (https://cwe.mitre.org/data/definitions/295.html)
Location: examples/no_host_key_verification.py:4
3 ssh_client = client.SSHClient()
4 ssh_client.set_missing_host_key_policy(client.AutoAddPolicy)
5 ssh_client.set_missing_host_key_policy(client.WarningPolicy)
.. versionadded:: 1.5.1
.. versionchanged:: 1.7.3
CWE information added
"""
import ast
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.checks("Call")
@test.test_id("B507")
def ssh_no_host_key_verification(context):
if (
context.is_module_imported_like("paramiko")
and context.call_function_name == "set_missing_host_key_policy"
and context.node.args
):
policy_argument = context.node.args[0]
policy_argument_value = None
if isinstance(policy_argument, ast.Attribute):
policy_argument_value = policy_argument.attr
elif isinstance(policy_argument, ast.Name):
policy_argument_value = policy_argument.id
elif isinstance(policy_argument, ast.Call):
if isinstance(policy_argument.func, ast.Attribute):
policy_argument_value = policy_argument.func.attr
elif isinstance(policy_argument.func, ast.Name):
policy_argument_value = policy_argument.func.id
if policy_argument_value in ["AutoAddPolicy", "WarningPolicy"]:
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.IMPROPER_CERT_VALIDATION,
text="Paramiko call with policy set to automatically trust "
"the unknown host key.",
lineno=context.get_lineno_for_call_arg(
"set_missing_host_key_policy"
),
)

View File

@@ -0,0 +1,121 @@
#
# SPDX-License-Identifier: Apache-2.0
#
r"""
=================================
B202: Test for tarfile.extractall
=================================
This plugin will look for usage of ``tarfile.extractall()``
Severity are set as follows:
* ``tarfile.extractall(members=function(tarfile))`` - LOW
* ``tarfile.extractall(members=?)`` - member is not a function - MEDIUM
* ``tarfile.extractall()`` - members from the archive is trusted - HIGH
Use ``tarfile.extractall(members=function_name)`` and define a function
that will inspect each member. Discard files that contain a directory
traversal sequences such as ``../`` or ``\..`` along with all special filetypes
unless you explicitly need them.
:Example:
.. code-block:: none
>> Issue: [B202:tarfile_unsafe_members] tarfile.extractall used without
any validation. You should check members and discard dangerous ones
Severity: High Confidence: High
CWE: CWE-22 (https://cwe.mitre.org/data/definitions/22.html)
Location: examples/tarfile_extractall.py:8
More Info:
https://bandit.readthedocs.io/en/latest/plugins/b202_tarfile_unsafe_members.html
7 tar = tarfile.open(filename)
8 tar.extractall(path=tempfile.mkdtemp())
9 tar.close()
.. seealso::
- https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall
- https://docs.python.org/3/library/tarfile.html#tarfile.TarInfo
.. versionadded:: 1.7.5
.. versionchanged:: 1.7.8
Added check for filter parameter
"""
import ast
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def exec_issue(level, members=""):
if level == bandit.LOW:
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.LOW,
cwe=issue.Cwe.PATH_TRAVERSAL,
text="Usage of tarfile.extractall(members=function(tarfile)). "
"Make sure your function properly discards dangerous members "
"{members}).".format(members=members),
)
elif level == bandit.MEDIUM:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.PATH_TRAVERSAL,
text="Found tarfile.extractall(members=?) but couldn't "
"identify the type of members. "
"Check if the members were properly validated "
"{members}).".format(members=members),
)
else:
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
cwe=issue.Cwe.PATH_TRAVERSAL,
text="tarfile.extractall used without any validation. "
"Please check and discard dangerous members.",
)
def get_members_value(context):
for keyword in context.node.keywords:
if keyword.arg == "members":
arg = keyword.value
if isinstance(arg, ast.Call):
return {"Function": arg.func.id}
else:
value = arg.id if isinstance(arg, ast.Name) else arg
return {"Other": value}
def is_filter_data(context):
for keyword in context.node.keywords:
if keyword.arg == "filter":
arg = keyword.value
return isinstance(arg, ast.Constant) and arg.value == "data"
@test.test_id("B202")
@test.checks("Call")
def tarfile_unsafe_members(context):
if all(
[
context.is_module_imported_exact("tarfile"),
"extractall" in context.call_function_name,
]
):
if "filter" in context.call_keywords and is_filter_data(context):
return None
if "members" in context.call_keywords:
members = get_members_value(context)
if "Function" in members:
return exec_issue(bandit.LOW, members)
else:
return exec_issue(bandit.MEDIUM, members)
return exec_issue(bandit.HIGH)

View File

@@ -0,0 +1,79 @@
#
# SPDX-License-Identifier: Apache-2.0
r"""
=====================================================
B613: TrojanSource - Bidirectional control characters
=====================================================
This plugin checks for the presence of unicode bidirectional control characters
in Python source files. Those characters can be embedded in comments and strings
to reorder source code characters in a way that changes its logic.
:Example:
.. code-block:: none
>> Issue: [B613:trojansource] A Python source file contains bidirectional control characters ('\u202e').
Severity: High Confidence: Medium
CWE: CWE-838 (https://cwe.mitre.org/data/definitions/838.html)
More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b113_trojansource.html
Location: examples/trojansource.py:4:25
3 access_level = "user"
4 if access_level != 'none': # Check if admin ' and access_level != 'user
5 print("You are an admin.\n")
.. seealso::
- https://trojansource.codes/
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574
.. versionadded:: 1.7.10
""" # noqa: E501
from tokenize import detect_encoding
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
BIDI_CHARACTERS = (
"\u202a",
"\u202b",
"\u202c",
"\u202d",
"\u202e",
"\u2066",
"\u2067",
"\u2068",
"\u2069",
"\u200f",
)
@test.test_id("B613")
@test.checks("File")
def trojansource(context):
with open(context.filename, "rb") as src_file:
encoding, _ = detect_encoding(src_file.readline)
with open(context.filename, encoding=encoding) as src_file:
for lineno, line in enumerate(src_file.readlines(), start=1):
for char in BIDI_CHARACTERS:
try:
col_offset = line.index(char) + 1
except ValueError:
continue
text = (
"A Python source file contains bidirectional"
" control characters (%r)." % char
)
b_issue = bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.INAPPROPRIATE_ENCODING_FOR_OUTPUT_CONTEXT,
text=text,
lineno=lineno,
col_offset=col_offset,
)
b_issue.linerange = [lineno]
return b_issue

View File

@@ -0,0 +1,108 @@
# Copyright 2016 IBM Corp.
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
=============================================
B112: Test for a continue in the except block
=============================================
Errors in Python code bases are typically communicated using ``Exceptions``.
An exception object is 'raised' in the event of an error and can be 'caught' at
a later point in the program, typically some error handling or logging action
will then be performed.
However, it is possible to catch an exception and silently ignore it while in
a loop. This is illustrated with the following example
.. code-block:: python
while keep_going:
try:
do_some_stuff()
except Exception:
continue
This pattern is considered bad practice in general, but also represents a
potential security issue. A larger than normal volume of errors from a service
can indicate an attempt is being made to disrupt or interfere with it. Thus
errors should, at the very least, be logged.
There are rare situations where it is desirable to suppress errors, but this is
typically done with specific exception types, rather than the base Exception
class (or no type). To accommodate this, the test may be configured to ignore
'try, except, continue' where the exception is typed. For example, the
following would not generate a warning if the configuration option
``checked_typed_exception`` is set to False:
.. code-block:: python
while keep_going:
try:
do_some_stuff()
except ZeroDivisionError:
continue
**Config Options:**
.. code-block:: yaml
try_except_continue:
check_typed_exception: True
:Example:
.. code-block:: none
>> Issue: Try, Except, Continue detected.
Severity: Low Confidence: High
CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
Location: ./examples/try_except_continue.py:5
4 a = i
5 except:
6 continue
.. seealso::
- https://security.openstack.org
- https://cwe.mitre.org/data/definitions/703.html
.. versionadded:: 1.0.0
.. versionchanged:: 1.7.3
CWE information added
"""
import ast
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def gen_config(name):
if name == "try_except_continue":
return {"check_typed_exception": False}
@test.takes_config
@test.checks("ExceptHandler")
@test.test_id("B112")
def try_except_continue(context, config):
node = context.node
if len(node.body) == 1:
if (
not config["check_typed_exception"]
and node.type is not None
and getattr(node.type, "id", None) != "Exception"
):
return
if isinstance(node.body[0], ast.Continue):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
cwe=issue.Cwe.IMPROPER_CHECK_OF_EXCEPT_COND,
text=("Try, Except, Continue detected."),
)

View File

@@ -0,0 +1,106 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
r"""
=========================================
B110: Test for a pass in the except block
=========================================
Errors in Python code bases are typically communicated using ``Exceptions``.
An exception object is 'raised' in the event of an error and can be 'caught' at
a later point in the program, typically some error handling or logging action
will then be performed.
However, it is possible to catch an exception and silently ignore it. This is
illustrated with the following example
.. code-block:: python
try:
do_some_stuff()
except Exception:
pass
This pattern is considered bad practice in general, but also represents a
potential security issue. A larger than normal volume of errors from a service
can indicate an attempt is being made to disrupt or interfere with it. Thus
errors should, at the very least, be logged.
There are rare situations where it is desirable to suppress errors, but this is
typically done with specific exception types, rather than the base Exception
class (or no type). To accommodate this, the test may be configured to ignore
'try, except, pass' where the exception is typed. For example, the following
would not generate a warning if the configuration option
``checked_typed_exception`` is set to False:
.. code-block:: python
try:
do_some_stuff()
except ZeroDivisionError:
pass
**Config Options:**
.. code-block:: yaml
try_except_pass:
check_typed_exception: True
:Example:
.. code-block:: none
>> Issue: Try, Except, Pass detected.
Severity: Low Confidence: High
CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
Location: ./examples/try_except_pass.py:4
3 a = 1
4 except:
5 pass
.. seealso::
- https://security.openstack.org
- https://cwe.mitre.org/data/definitions/703.html
.. versionadded:: 0.13.0
.. versionchanged:: 1.7.3
CWE information added
"""
import ast
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def gen_config(name):
if name == "try_except_pass":
return {"check_typed_exception": False}
@test.takes_config
@test.checks("ExceptHandler")
@test.test_id("B110")
def try_except_pass(context, config):
node = context.node
if len(node.body) == 1:
if (
not config["check_typed_exception"]
and node.type is not None
and getattr(node.type, "id", None) != "Exception"
):
return
if isinstance(node.body[0], ast.Pass):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
cwe=issue.Cwe.IMPROPER_CHECK_OF_EXCEPT_COND,
text=("Try, Except, Pass detected."),
)

View File

@@ -0,0 +1,165 @@
# Copyright (c) 2015 VMware, Inc.
#
# SPDX-License-Identifier: Apache-2.0
r"""
=========================================
B505: Test for weak cryptographic key use
=========================================
As computational power increases, so does the ability to break ciphers with
smaller key lengths. The recommended key length size for RSA and DSA algorithms
is 2048 and higher. 1024 bits and below are now considered breakable. EC key
length sizes are recommended to be 224 and higher with 160 and below considered
breakable. This plugin test checks for use of any key less than those limits
and returns a high severity error if lower than the lower threshold and a
medium severity error for those lower than the higher threshold.
:Example:
.. code-block:: none
>> Issue: DSA key sizes below 1024 bits are considered breakable.
Severity: High Confidence: High
CWE: CWE-326 (https://cwe.mitre.org/data/definitions/326.html)
Location: examples/weak_cryptographic_key_sizes.py:36
35 # Also incorrect: without keyword args
36 dsa.generate_private_key(512,
37 backends.default_backend())
38 rsa.generate_private_key(3,
.. seealso::
- https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
- https://security.openstack.org/guidelines/dg_strong-crypto.html
- https://cwe.mitre.org/data/definitions/326.html
.. versionadded:: 0.14.0
.. versionchanged:: 1.7.3
CWE information added
"""
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
def gen_config(name):
if name == "weak_cryptographic_key":
return {
"weak_key_size_dsa_high": 1024,
"weak_key_size_dsa_medium": 2048,
"weak_key_size_rsa_high": 1024,
"weak_key_size_rsa_medium": 2048,
"weak_key_size_ec_high": 160,
"weak_key_size_ec_medium": 224,
}
def _classify_key_size(config, key_type, key_size):
if isinstance(key_size, str):
# size provided via a variable - can't process it at the moment
return
key_sizes = {
"DSA": [
(config["weak_key_size_dsa_high"], bandit.HIGH),
(config["weak_key_size_dsa_medium"], bandit.MEDIUM),
],
"RSA": [
(config["weak_key_size_rsa_high"], bandit.HIGH),
(config["weak_key_size_rsa_medium"], bandit.MEDIUM),
],
"EC": [
(config["weak_key_size_ec_high"], bandit.HIGH),
(config["weak_key_size_ec_medium"], bandit.MEDIUM),
],
}
for size, level in key_sizes[key_type]:
if key_size < size:
return bandit.Issue(
severity=level,
confidence=bandit.HIGH,
cwe=issue.Cwe.INADEQUATE_ENCRYPTION_STRENGTH,
text="%s key sizes below %d bits are considered breakable. "
% (key_type, size),
)
def _weak_crypto_key_size_cryptography_io(context, config):
func_key_type = {
"cryptography.hazmat.primitives.asymmetric.dsa."
"generate_private_key": "DSA",
"cryptography.hazmat.primitives.asymmetric.rsa."
"generate_private_key": "RSA",
"cryptography.hazmat.primitives.asymmetric.ec."
"generate_private_key": "EC",
}
arg_position = {
"DSA": 0,
"RSA": 1,
"EC": 0,
}
key_type = func_key_type.get(context.call_function_name_qual)
if key_type in ["DSA", "RSA"]:
key_size = (
context.get_call_arg_value("key_size")
or context.get_call_arg_at_position(arg_position[key_type])
or 2048
)
return _classify_key_size(config, key_type, key_size)
elif key_type == "EC":
curve_key_sizes = {
"SECT571K1": 571,
"SECT571R1": 570,
"SECP521R1": 521,
"BrainpoolP512R1": 512,
"SECT409K1": 409,
"SECT409R1": 409,
"BrainpoolP384R1": 384,
"SECP384R1": 384,
"SECT283K1": 283,
"SECT283R1": 283,
"BrainpoolP256R1": 256,
"SECP256K1": 256,
"SECP256R1": 256,
"SECT233K1": 233,
"SECT233R1": 233,
"SECP224R1": 224,
"SECP192R1": 192,
"SECT163K1": 163,
"SECT163R2": 163,
}
curve = context.get_call_arg_value("curve") or (
len(context.call_args) > arg_position[key_type]
and context.call_args[arg_position[key_type]]
)
key_size = curve_key_sizes[curve] if curve in curve_key_sizes else 224
return _classify_key_size(config, key_type, key_size)
def _weak_crypto_key_size_pycrypto(context, config):
func_key_type = {
"Crypto.PublicKey.DSA.generate": "DSA",
"Crypto.PublicKey.RSA.generate": "RSA",
"Cryptodome.PublicKey.DSA.generate": "DSA",
"Cryptodome.PublicKey.RSA.generate": "RSA",
}
key_type = func_key_type.get(context.call_function_name_qual)
if key_type:
key_size = (
context.get_call_arg_value("bits")
or context.get_call_arg_at_position(0)
or 2048
)
return _classify_key_size(config, key_type, key_size)
@test.takes_config
@test.checks("Call")
@test.test_id("B505")
def weak_cryptographic_key(context, config):
return _weak_crypto_key_size_cryptography_io(
context, config
) or _weak_crypto_key_size_pycrypto(context, config)

View File

@@ -0,0 +1,76 @@
#
# Copyright (c) 2016 Rackspace, Inc.
#
# SPDX-License-Identifier: Apache-2.0
r"""
===============================
B506: Test for use of yaml load
===============================
This plugin test checks for the unsafe usage of the ``yaml.load`` function from
the PyYAML package. The yaml.load function provides the ability to construct
an arbitrary Python object, which may be dangerous if you receive a YAML
document from an untrusted source. The function yaml.safe_load limits this
ability to simple Python objects like integers or lists.
Please see
https://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML for more information
on ``yaml.load`` and yaml.safe_load
:Example:
.. code-block:: none
>> Issue: [yaml_load] Use of unsafe yaml load. Allows instantiation of
arbitrary objects. Consider yaml.safe_load().
Severity: Medium Confidence: High
CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html)
Location: examples/yaml_load.py:5
4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})
5 y = yaml.load(ystr)
6 yaml.dump(y)
.. seealso::
- https://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML
- https://cwe.mitre.org/data/definitions/20.html
.. versionadded:: 1.0.0
.. versionchanged:: 1.7.3
CWE information added
"""
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
@test.test_id("B506")
@test.checks("Call")
def yaml_load(context):
imported = context.is_module_imported_exact("yaml")
qualname = context.call_function_name_qual
if not imported and isinstance(qualname, str):
return
qualname_list = qualname.split(".")
func = qualname_list[-1]
if all(
[
"yaml" in qualname_list,
func == "load",
not context.check_call_arg_value("Loader", "SafeLoader"),
not context.check_call_arg_value("Loader", "CSafeLoader"),
not context.get_call_arg_at_position(1) == "SafeLoader",
not context.get_call_arg_at_position(1) == "CSafeLoader",
]
):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.IMPROPER_INPUT_VALIDATION,
text="Use of unsafe yaml load. Allows instantiation of"
" arbitrary objects. Consider yaml.safe_load().",
lineno=context.node.lineno,
)