Skip to content

Commit 72fa5a7

Browse files
authored
Improve handling nosec for multi-line strings (#915)
This commit improves handling nosecs in multi-line strings, like: 1. nosec_not_working = f""" 2. SELECT * FROM {table} 3. """ # nosec Before this change, bandit was checking if there is a nosec in line 1. Now, it searches for nosec in all lines of the expression. In python 3.7, linerange for a multiline expression is sqeezed to first line. Thus, if nosec is set in the second or further line then it is not taken into account by bandit. This commit also moves detecting nosec without test number to test phase from "pre-visit" phase. It may increase the time of performing checks but avoids counting the same nosec mark multiple times. "pre-visit" phase is run separately for each part of multi-line string split by FormattedValue items. Thus for the above example, it would be run twice, the first time for "\n SELECT * FROM " and the second time for "\n" making the nosec being counted twice. Resolves: #880
1 parent 7e6f580 commit 72fa5a7

File tree

5 files changed

+100
-16
lines changed

5 files changed

+100
-16
lines changed

bandit/core/node_visitor.py

-7
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,6 @@ def pre_visit(self, node):
200200
if hasattr(node, "lineno"):
201201
self.context["lineno"] = node.lineno
202202

203-
# explicitly check for empty set to skip all tests for a line
204-
nosec_tests = self.nosec_lines.get(node.lineno)
205-
if nosec_tests is not None and not len(nosec_tests):
206-
LOG.debug("skipped, nosec without test number")
207-
self.metrics.note_nosec()
208-
return False
209-
210203
if hasattr(node, "col_offset"):
211204
self.context["col_offset"] = node.col_offset
212205
if hasattr(node, "end_col_offset"):

bandit/core/tester.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ def run_tests(self, raw_context, checktype):
7676

7777
# don't skip the test if there was no nosec comment
7878
if nosec_tests_to_skip is not None:
79-
# if the set is empty or the test id is in the set of
80-
# tests to skip, log and increment the skip by test
81-
# count
82-
if not nosec_tests_to_skip or (
83-
result.test_id in nosec_tests_to_skip
84-
):
79+
# If the set is empty then it means that nosec was
80+
# used without test number -> update nosecs counter.
81+
# If the test id is in the set of tests to skip,
82+
# log and increment the skip by test count.
83+
if not nosec_tests_to_skip:
84+
LOG.debug("skipped, nosec without test number")
85+
self.metrics.note_nosec()
86+
continue
87+
elif result.test_id in nosec_tests_to_skip:
8588
LOG.debug(
8689
"skipped, nosec for test %s" % result.test_id
8790
)
@@ -129,7 +132,7 @@ def _get_nosecs_from_contexts(self, context, test_result=None):
129132
if test_result
130133
else None
131134
)
132-
context_tests = self.nosec_lines.get(context["lineno"], None)
135+
context_tests = utils.get_nosec(self.nosec_lines, context)
133136

134137
# if both are none there were no comments
135138
# this is explicitly different from being empty.

bandit/core/utils.py

+8
Original file line numberDiff line numberDiff line change
@@ -370,3 +370,11 @@ def check_ast_node(name):
370370
pass
371371

372372
raise TypeError("Error: %s is not a valid node type in AST" % name)
373+
374+
375+
def get_nosec(nosec_lines, context):
376+
for lineno in context["linerange"]:
377+
nosec = nosec_lines.get(lineno, None)
378+
if nosec is not None:
379+
return nosec
380+
return None

examples/sql_multiline_statements.py

+59
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,62 @@ def b():
121121

122122
a()("""SELECT %s
123123
FROM foo""" % val)
124+
125+
# skip
126+
query = """SELECT *
127+
FROM foo WHERE id = '%s'""" % identifier # nosec
128+
query = """SELECT *
129+
FROM foo WHERE id = '%s'""" % identifier # nosec B608
130+
query = """
131+
SELECT *
132+
FROM foo
133+
WHERE id = '%s'
134+
""" % identifier # nosec B608
135+
136+
query = f"""
137+
SELECT *
138+
FROM foo
139+
WHERE id = {identifier}
140+
""" # nosec
141+
query = f"""
142+
SELECT *
143+
FROM foo
144+
WHERE id = {identifier}
145+
""" # nosec B608
146+
147+
query = f"""
148+
SELECT *
149+
FROM foo
150+
WHERE id = {identifier}""" # nosec
151+
query = f"""
152+
SELECT *
153+
FROM foo
154+
WHERE id = {identifier}""" # nosec B608
155+
156+
cur.execute("SELECT * " # nosec
157+
"FROM foo "
158+
f"WHERE id = {identifier}")
159+
cur.execute("SELECT * " # nosec B608
160+
"FROM foo "
161+
f"WHERE id = {identifier}")
162+
163+
query = ("SELECT * " # nosec
164+
"FROM foo "
165+
f"WHERE id = {identifier}")
166+
query = ("SELECT * " # nosec B608
167+
"FROM foo "
168+
f"WHERE id = {identifier}")
169+
170+
# nosec is not recognized for the 4 below cases in python 3.7
171+
query = ("SELECT * "
172+
"FROM foo " # nosec
173+
f"WHERE id = {identifier}")
174+
query = ("SELECT * "
175+
"FROM foo " # nosec B608
176+
f"WHERE id = {identifier}")
177+
query = ("SELECT * "
178+
"FROM foo "
179+
f"WHERE id = {identifier}") # nosec
180+
query = ("SELECT * "
181+
"FROM foo "
182+
f"WHERE id = {identifier}") # nosec B608

tests/functional/test_functional.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -457,21 +457,42 @@ def test_multiline_sql_statements(self):
457457
multi-line strings.
458458
"""
459459
example_file = "sql_multiline_statements.py"
460+
confidence_low_tests = 13
461+
severity_medium_tests = 26
462+
nosec_tests = 7
463+
skipped_tests = 8
464+
if sys.version_info[:2] <= (3, 7):
465+
# In the case of implicit concatenation in python 3.7,
466+
# we know only the first line of multi-line string.
467+
# Thus, cases like:
468+
# query = ("SELECT * "
469+
# "FROM foo " # nosec
470+
# f"WHERE id = {identifier}")
471+
# are not skipped but reported as errors.
472+
confidence_low_tests = 17
473+
severity_medium_tests = 30
474+
nosec_tests = 5
475+
skipped_tests = 6
460476
expect = {
461477
"SEVERITY": {
462478
"UNDEFINED": 0,
463479
"LOW": 0,
464-
"MEDIUM": 26,
480+
"MEDIUM": severity_medium_tests,
465481
"HIGH": 0,
466482
},
467483
"CONFIDENCE": {
468484
"UNDEFINED": 0,
469-
"LOW": 13,
485+
"LOW": confidence_low_tests,
470486
"MEDIUM": 13,
471487
"HIGH": 0,
472488
},
473489
}
490+
expect_stats = {
491+
"nosec": nosec_tests,
492+
"skipped_tests": skipped_tests,
493+
}
474494
self.check_example(example_file, expect)
495+
self.check_metrics(example_file, expect_stats)
475496

476497
def test_ssl_insecure_version(self):
477498
"""Test for insecure SSL protocol versions."""

0 commit comments

Comments
 (0)