updates
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"),
|
||||
)
|
||||
@@ -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."
|
||||
),
|
||||
)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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 }.",
|
||||
)
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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"
|
||||
),
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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."),
|
||||
)
|
||||
@@ -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."),
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user