Skip to content

Commit 7e6f580

Browse files
authored
Improve detecting SQL injections in f-strings (#917)
* Improve detecting SQL injections in f-strings This commit fixes detecting SQL injection in statements like: f"SELECT {column_name} FROM foo WHERE id = 1" f"INSERT INTO {table_name} VALUES (1)" f"UPDATE {table_name} SET id = 1" Before this change, the bandit was analyzing statements by parts, especially, in the case of: "SELECT {column_name} FROM foo WHERE id = 1" it was firstly checking "SELECT " for being an SQL statement and then " FROM foo WHERE id = 1". Neither of these parts match to defined regular expressions: r"(select\s.*from\s|" r"delete\s+from\s|" r"insert\s+into\s.*values\s|" r"update\s.*set\s)", Thus SQL injection was not detected. This commit makes bandit checking the whole SQL statement for matching the above regexps. Resolves: #916 * Add tests for multiline strings
1 parent fe1361f commit 7e6f580

File tree

4 files changed

+165
-4
lines changed

4 files changed

+165
-4
lines changed

bandit/plugins/injection_sql.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,18 @@ def _evaluate_ast(node):
9292
elif hasattr(ast, "JoinedStr") and isinstance(
9393
node._bandit_parent, ast.JoinedStr
9494
):
95-
statement = node.s
96-
wrapper = node._bandit_parent._bandit_parent
95+
substrings = [
96+
child
97+
for child in node._bandit_parent.values
98+
if isinstance(child, ast.Str)
99+
]
100+
# JoinedStr consists of list of Constant and FormattedValue
101+
# instances. Let's perform one test for the whole string
102+
# and abandon all parts except the first one to raise one
103+
# failed test instead of many for the same SQL statement.
104+
if substrings and node == substrings[0]:
105+
statement = "".join([str(child.s) for child in substrings])
106+
wrapper = node._bandit_parent._bandit_parent
97107

98108
if isinstance(wrapper, ast.Call): # wrapped in "execute" call?
99109
names = ["execute", "executemany"]

examples/sql_multiline_statements.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import sqlalchemy
2+
3+
# bad
4+
query = """SELECT *
5+
FROM foo WHERE id = '%s'""" % identifier
6+
query = """INSERT INTO foo
7+
VALUES ('a', 'b', '%s')""" % value
8+
query = """DELETE FROM foo
9+
WHERE id = '%s'""" % identifier
10+
query = """UPDATE foo
11+
SET value = 'b'
12+
WHERE id = '%s'""" % identifier
13+
query = """WITH cte AS (SELECT x FROM foo)
14+
SELECT x FROM cte WHERE x = '%s'""" % identifier
15+
# bad alternate forms
16+
query = """SELECT *
17+
FROM foo
18+
WHERE id = '""" + identifier + "'"
19+
query = """SELECT *
20+
FROM foo
21+
WHERE id = '{}'""".format(identifier)
22+
23+
query = f"""
24+
SELECT *
25+
FROM foo
26+
WHERE id = {identifier}
27+
"""
28+
29+
# bad
30+
cur.execute("""SELECT *
31+
FROM foo
32+
WHERE id = '%s'""" % identifier)
33+
cur.execute("""INSERT INTO foo
34+
VALUES ('a', 'b', '%s')""" % value)
35+
cur.execute("""DELETE FROM foo
36+
WHERE id = '%s'""" % identifier)
37+
cur.execute("""UPDATE foo
38+
SET value = 'b'
39+
WHERE id = '%s'""" % identifier)
40+
# bad alternate forms
41+
cur.execute("""SELECT *
42+
FROM foo
43+
WHERE id = '""" + identifier + "'")
44+
cur.execute("""SELECT *
45+
FROM foo
46+
WHERE id = '{}'""".format(identifier))
47+
48+
# bad with f-string
49+
query = f"""
50+
SELECT *
51+
FROM foo
52+
WHERE id = {identifier}
53+
"""
54+
query = f"""
55+
SELECT *
56+
FROM foo
57+
WHERE id = {identifier}
58+
"""
59+
60+
query = f"""
61+
SELECT *
62+
FROM foo
63+
WHERE id = {identifier}"""
64+
query = f"""
65+
SELECT *
66+
FROM foo
67+
WHERE id = {identifier}"""
68+
69+
cur.execute(f"""
70+
SELECT
71+
{column_name}
72+
FROM foo
73+
WHERE id = 1""")
74+
75+
cur.execute(f"""
76+
SELECT
77+
{a + b}
78+
FROM foo
79+
WHERE id = 1""")
80+
81+
cur.execute(f"""
82+
INSERT INTO
83+
{table_name}
84+
VALUES (1)""")
85+
cur.execute(f"""
86+
UPDATE {table_name}
87+
SET id = 1""")
88+
89+
# implicit concatenation mixed with f-strings
90+
cur.execute("SELECT "
91+
f"{column_name} "
92+
"FROM foo "
93+
"WHERE id = 1"
94+
)
95+
cur.execute("INSERT INTO "
96+
f"{table_name} "
97+
"VALUES (1)")
98+
cur.execute(f"UPDATE {table_name} "
99+
"SET id = 1")
100+
101+
# good
102+
cur.execute("""SELECT *
103+
FROM foo
104+
WHERE id = '%s'""", identifier)
105+
cur.execute("""INSERT INTO foo
106+
VALUES ('a', 'b', '%s')""", value)
107+
cur.execute("""DELETE FROM foo
108+
WHERE id = '%s'""", identifier)
109+
cur.execute("""UPDATE foo
110+
SET value = 'b'
111+
WHERE id = '%s'""", identifier)
112+
113+
114+
# bug: https://bugs.launchpad.net/bandit/+bug/1479625
115+
def a():
116+
def b():
117+
pass
118+
119+
return b
120+
121+
122+
a()("""SELECT %s
123+
FROM foo""" % val)

examples/sql_statements.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
cur.execute("SELECT * FROM foo WHERE id = '" + identifier + "'")
2121
cur.execute("SELECT * FROM foo WHERE id = '{}'".format(identifier))
2222

23+
# bad f-strings
24+
cur.execute(f"SELECT {column_name} FROM foo WHERE id = 1")
25+
cur.execute(f"SELECT {a + b} FROM foo WHERE id = 1")
26+
cur.execute(f"INSERT INTO {table_name} VALUES (1)")
27+
cur.execute(f"UPDATE {table_name} SET id = 1")
28+
2329
# good
2430
cur.execute("SELECT * FROM foo WHERE id = '%s'", identifier)
2531
cur.execute("INSERT INTO foo VALUES ('a', 'b', '%s')", value)

tests/functional/test_functional.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,18 +439,40 @@ def test_sql_statements(self):
439439
"SEVERITY": {
440440
"UNDEFINED": 0,
441441
"LOW": 0,
442-
"MEDIUM": 14,
442+
"MEDIUM": 18,
443443
"HIGH": 0,
444444
},
445445
"CONFIDENCE": {
446446
"UNDEFINED": 0,
447447
"LOW": 8,
448-
"MEDIUM": 6,
448+
"MEDIUM": 10,
449449
"HIGH": 0,
450450
},
451451
}
452452
self.check_example("sql_statements.py", expect)
453453

454+
def test_multiline_sql_statements(self):
455+
"""
456+
Test for SQL injection through string building using
457+
multi-line strings.
458+
"""
459+
example_file = "sql_multiline_statements.py"
460+
expect = {
461+
"SEVERITY": {
462+
"UNDEFINED": 0,
463+
"LOW": 0,
464+
"MEDIUM": 26,
465+
"HIGH": 0,
466+
},
467+
"CONFIDENCE": {
468+
"UNDEFINED": 0,
469+
"LOW": 13,
470+
"MEDIUM": 13,
471+
"HIGH": 0,
472+
},
473+
}
474+
self.check_example(example_file, expect)
475+
454476
def test_ssl_insecure_version(self):
455477
"""Test for insecure SSL protocol versions."""
456478
expect = {

0 commit comments

Comments
 (0)