injection_sql.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. #
  2. # Copyright 2014 Hewlett-Packard Development Company, L.P.
  3. #
  4. # SPDX-License-Identifier: Apache-2.0
  5. r"""
  6. ============================
  7. B608: Test for SQL injection
  8. ============================
  9. An SQL injection attack consists of insertion or "injection" of a SQL query via
  10. the input data given to an application. It is a very common attack vector. This
  11. plugin test looks for strings that resemble SQL statements that are involved in
  12. some form of string building operation. For example:
  13. - "SELECT %s FROM derp;" % var
  14. - "SELECT thing FROM " + tab
  15. - "SELECT " + val + " FROM " + tab + ...
  16. - "SELECT {} FROM derp;".format(var)
  17. - f"SELECT foo FROM bar WHERE id = {product}"
  18. Unless care is taken to sanitize and control the input data when building such
  19. SQL statement strings, an injection attack becomes possible. If strings of this
  20. nature are discovered, a LOW confidence issue is reported. In order to boost
  21. result confidence, this plugin test will also check to see if the discovered
  22. string is in use with standard Python DBAPI calls `execute` or `executemany`.
  23. If so, a MEDIUM issue is reported. For example:
  24. - cursor.execute("SELECT %s FROM derp;" % var)
  25. :Example:
  26. .. code-block:: none
  27. >> Issue: Possible SQL injection vector through string-based query
  28. construction.
  29. Severity: Medium Confidence: Low
  30. CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
  31. Location: ./examples/sql_statements.py:4
  32. 3 query = "DELETE FROM foo WHERE id = '%s'" % identifier
  33. 4 query = "UPDATE foo SET value = 'b' WHERE id = '%s'" % identifier
  34. 5
  35. .. seealso::
  36. - https://www.owasp.org/index.php/SQL_Injection
  37. - https://security.openstack.org/guidelines/dg_parameterize-database-queries.html
  38. - https://cwe.mitre.org/data/definitions/89.html
  39. .. versionadded:: 0.9.0
  40. .. versionchanged:: 1.7.3
  41. CWE information added
  42. """ # noqa: E501
  43. import ast
  44. import re
  45. import bandit
  46. from bandit.core import issue
  47. from bandit.core import test_properties as test
  48. from bandit.core import utils
  49. SIMPLE_SQL_RE = re.compile(
  50. r"(select\s.*from\s|"
  51. r"delete\s+from\s|"
  52. r"insert\s+into\s.*values\s|"
  53. r"update\s.*set\s)",
  54. re.IGNORECASE | re.DOTALL,
  55. )
  56. def _check_string(data):
  57. return SIMPLE_SQL_RE.search(data) is not None
  58. def _evaluate_ast(node):
  59. wrapper = None
  60. statement = ""
  61. if isinstance(node._bandit_parent, ast.BinOp):
  62. out = utils.concat_string(node, node._bandit_parent)
  63. wrapper = out[0]._bandit_parent
  64. statement = out[1]
  65. elif (
  66. isinstance(node._bandit_parent, ast.Attribute)
  67. and node._bandit_parent.attr == "format"
  68. ):
  69. statement = node.s
  70. # Hierarchy for "".format() is Wrapper -> Call -> Attribute -> Str
  71. wrapper = node._bandit_parent._bandit_parent._bandit_parent
  72. elif hasattr(ast, "JoinedStr") and isinstance(
  73. node._bandit_parent, ast.JoinedStr
  74. ):
  75. substrings = [
  76. child
  77. for child in node._bandit_parent.values
  78. if isinstance(child, ast.Str)
  79. ]
  80. # JoinedStr consists of list of Constant and FormattedValue
  81. # instances. Let's perform one test for the whole string
  82. # and abandon all parts except the first one to raise one
  83. # failed test instead of many for the same SQL statement.
  84. if substrings and node == substrings[0]:
  85. statement = "".join([str(child.s) for child in substrings])
  86. wrapper = node._bandit_parent._bandit_parent
  87. if isinstance(wrapper, ast.Call): # wrapped in "execute" call?
  88. names = ["execute", "executemany"]
  89. name = utils.get_called_name(wrapper)
  90. return (name in names, statement)
  91. else:
  92. return (False, statement)
  93. @test.checks("Str")
  94. @test.test_id("B608")
  95. def hardcoded_sql_expressions(context):
  96. val = _evaluate_ast(context.node)
  97. if _check_string(val[1]):
  98. return bandit.Issue(
  99. severity=bandit.MEDIUM,
  100. confidence=bandit.MEDIUM if val[0] else bandit.LOW,
  101. cwe=issue.Cwe.SQL_INJECTION,
  102. text="Possible SQL injection vector through string-based "
  103. "query construction.",
  104. )