Skip to content

Commit fe9ca8b

Browse files
costaparassigmavirus24ericwb
authored
Flag str.replace as possible sql injection (#1044)
* Flag str.replace as possible sql injection This extends the existing implementation for detecting possible cases of SQL injection to account for `str.replace` used in the string construction. Use of `str.replace` can lead to SQL injection in much the same way as `str.format` can, and that is already considered in the pre-existing implementation, along with other common string constructions. Resolves #878 * Revert cosmetic change * Fix lint * Reduce str.replace to LOW confidence in all cases Since the rate of false positives may be higher for str.replace over other string constructions like str.format, we should reduce to LOW confidence to compensate for this. * Update bandit/plugins/injection_sql.py Correct version in versionchanged directive Co-authored-by: Eric Brown <[email protected]> * Fix typo in comment --------- Co-authored-by: Ian Stapleton Cordasco <[email protected]> Co-authored-by: Eric Brown <[email protected]>
1 parent 5ec806d commit fe9ca8b

File tree

3 files changed

+27
-11
lines changed

3 files changed

+27
-11
lines changed

bandit/plugins/injection_sql.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
2828
- cursor.execute("SELECT %s FROM derp;" % var)
2929
30+
Use of str.replace in the string construction can also be dangerous.
31+
For example:
32+
33+
- "SELECT * FROM foo WHERE id = '[VALUE]'".replace("[VALUE]", identifier)
34+
35+
However, such cases are always reported with LOW confidence to compensate
36+
for false positives, since valid uses of str.replace can be common.
3037
3138
:Example:
3239
@@ -52,6 +59,9 @@
5259
.. versionchanged:: 1.7.3
5360
CWE information added
5461
62+
.. versionchanged:: 1.7.7
63+
Flag when str.replace is used in the string construction
64+
5565
""" # noqa: E501
5666
import ast
5767
import re
@@ -77,18 +87,20 @@ def _check_string(data):
7787
def _evaluate_ast(node):
7888
wrapper = None
7989
statement = ""
90+
str_replace = False
8091

8192
if isinstance(node._bandit_parent, ast.BinOp):
8293
out = utils.concat_string(node, node._bandit_parent)
8394
wrapper = out[0]._bandit_parent
8495
statement = out[1]
85-
elif (
86-
isinstance(node._bandit_parent, ast.Attribute)
87-
and node._bandit_parent.attr == "format"
88-
):
96+
elif isinstance(
97+
node._bandit_parent, ast.Attribute
98+
) and node._bandit_parent.attr in ("format", "replace"):
8999
statement = node.s
90100
# Hierarchy for "".format() is Wrapper -> Call -> Attribute -> Str
91101
wrapper = node._bandit_parent._bandit_parent._bandit_parent
102+
if node._bandit_parent.attr == "replace":
103+
str_replace = True
92104
elif hasattr(ast, "JoinedStr") and isinstance(
93105
node._bandit_parent, ast.JoinedStr
94106
):
@@ -108,19 +120,21 @@ def _evaluate_ast(node):
108120
if isinstance(wrapper, ast.Call): # wrapped in "execute" call?
109121
names = ["execute", "executemany"]
110122
name = utils.get_called_name(wrapper)
111-
return (name in names, statement)
123+
return (name in names, statement, str_replace)
112124
else:
113-
return (False, statement)
125+
return (False, statement, str_replace)
114126

115127

116128
@test.checks("Str")
117129
@test.test_id("B608")
118130
def hardcoded_sql_expressions(context):
119-
val = _evaluate_ast(context.node)
120-
if _check_string(val[1]):
131+
execute_call, statement, str_replace = _evaluate_ast(context.node)
132+
if _check_string(statement):
121133
return bandit.Issue(
122134
severity=bandit.MEDIUM,
123-
confidence=bandit.MEDIUM if val[0] else bandit.LOW,
135+
confidence=bandit.MEDIUM
136+
if execute_call and not str_replace
137+
else bandit.LOW,
124138
cwe=issue.Cwe.SQL_INJECTION,
125139
text="Possible SQL injection vector through string-based "
126140
"query construction.",

examples/sql_statements.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# bad alternate forms
1111
query = "SELECT * FROM foo WHERE id = '" + identifier + "'"
1212
query = "SELECT * FROM foo WHERE id = '{}'".format(identifier)
13+
query = "SELECT * FROM foo WHERE id = '[VALUE]'".replace("[VALUE]", identifier)
1314

1415
# bad
1516
cur.execute("SELECT * FROM foo WHERE id = '%s'" % identifier)
@@ -19,6 +20,7 @@
1920
# bad alternate forms
2021
cur.execute("SELECT * FROM foo WHERE id = '" + identifier + "'")
2122
cur.execute("SELECT * FROM foo WHERE id = '{}'".format(identifier))
23+
cur.execute("SELECT * FROM foo WHERE id = '[VALUE]'".replace("[VALUE]", identifier))
2224

2325
# bad f-strings
2426
cur.execute(f"SELECT {column_name} FROM foo WHERE id = 1")

tests/functional/test_functional.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,12 @@ def test_sql_statements(self):
439439
"SEVERITY": {
440440
"UNDEFINED": 0,
441441
"LOW": 0,
442-
"MEDIUM": 18,
442+
"MEDIUM": 20,
443443
"HIGH": 0,
444444
},
445445
"CONFIDENCE": {
446446
"UNDEFINED": 0,
447-
"LOW": 8,
447+
"LOW": 10,
448448
"MEDIUM": 10,
449449
"HIGH": 0,
450450
},

0 commit comments

Comments
 (0)