144 lines
4.8 KiB
Python
144 lines
4.8 KiB
Python
#
|
|
# 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.",
|
|
)
|