Skip to content

Commit 5c30350

Browse files
authored
Support of Python 3.14 (#1323)
* Support of Python 3.14 This is a re-opening of PR #1189 and revert of revert #1217. PR #1189 caused issue #1216 which must be fixed as part of this PR. This change starts testing against Python 3.14 now that is has been officially released. Python 3.14 has dropped the deprecated use of ast.Bytes, ast.Ellipsis, ast.NameConstant, ast.Num, ast.Str. They are replaced with ast.Constant and Node.value is used to get the value instead of the previous attributes like Node.s. https://docs.python.org/3.14/whatsnew/3.14.html#id2 This also has the potential to break 3rd party plugins that were checking on Str or Num, etc. As a result, Bandit keeps the validity of checking on those non-existent ast types. These changes did break a quite a few plugins that were directly accessing ast classes to determine a result, but were fixed as part of this PR. Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com> * Add 3.14 classifier Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com> * Add test case Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com> * Check if value.value is str Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com> * Incorrect comment Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com> * Fix up injection_sql.py Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com> * More checking on Constant.value Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com> * Final Constant value checks Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com> --------- Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com>
1 parent e1ffdf6 commit 5c30350

File tree

14 files changed

+142
-79
lines changed

14 files changed

+142
-79
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
["3.11", "311"],
5353
["3.12", "312"],
5454
["3.13", "313"],
55+
["3.14", "314"],
5556
]
5657
os: [ubuntu-latest, macos-latest]
5758
runs-on: ${{ matrix.os }}

bandit/core/blacklisting.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ def blacklist(context, config):
3535
func = context.node.func
3636
if isinstance(func, ast.Name) and func.id == "__import__":
3737
if len(context.node.args):
38-
if isinstance(context.node.args[0], ast.Str):
39-
name = context.node.args[0].s
38+
if isinstance(
39+
context.node.args[0], ast.Constant
40+
) and isinstance(context.node.args[0].value, str):
41+
name = context.node.args[0].value
4042
else:
4143
# TODO(??): import through a variable, need symbol tab
4244
name = "UNKNOWN"

bandit/core/context.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,13 @@ def _get_literal_value(self, literal):
178178
:param literal: The AST literal to convert
179179
:return: The value of the AST literal
180180
"""
181-
if isinstance(literal, ast.Num):
182-
literal_value = literal.n
183-
184-
elif isinstance(literal, ast.Str):
185-
literal_value = literal.s
181+
if isinstance(literal, ast.Constant):
182+
if isinstance(literal.value, bool):
183+
literal_value = str(literal.value)
184+
elif literal.value is None:
185+
literal_value = str(literal.value)
186+
else:
187+
literal_value = literal.value
186188

187189
elif isinstance(literal, ast.List):
188190
return_list = list()
@@ -205,19 +207,9 @@ def _get_literal_value(self, literal):
205207
elif isinstance(literal, ast.Dict):
206208
literal_value = dict(zip(literal.keys, literal.values))
207209

208-
elif isinstance(literal, ast.Ellipsis):
209-
# what do we want to do with this?
210-
literal_value = None
211-
212210
elif isinstance(literal, ast.Name):
213211
literal_value = literal.id
214212

215-
elif isinstance(literal, ast.NameConstant):
216-
literal_value = str(literal.value)
217-
218-
elif isinstance(literal, ast.Bytes):
219-
literal_value = literal.s
220-
221213
else:
222214
literal_value = None
223215

bandit/core/node_visitor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def visit_Str(self, node):
168168
:param node: The node that is being inspected
169169
:return: -
170170
"""
171-
self.context["str"] = node.s
171+
self.context["str"] = node.value
172172
if not isinstance(node._bandit_parent, ast.Expr): # docstring
173173
self.context["linerange"] = b_utils.linerange(node._bandit_parent)
174174
self.update_scores(self.tester.run_tests(self.context, "Str"))
@@ -181,7 +181,7 @@ def visit_Bytes(self, node):
181181
:param node: The node that is being inspected
182182
:return: -
183183
"""
184-
self.context["bytes"] = node.s
184+
self.context["bytes"] = node.value
185185
if not isinstance(node._bandit_parent, ast.Expr): # docstring
186186
self.context["linerange"] = b_utils.linerange(node._bandit_parent)
187187
self.update_scores(self.tester.run_tests(self.context, "Bytes"))

bandit/core/utils.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,12 @@ def linerange(node):
273273
def concat_string(node, stop=None):
274274
"""Builds a string from a ast.BinOp chain.
275275
276-
This will build a string from a series of ast.Str nodes wrapped in
276+
This will build a string from a series of ast.Constant nodes wrapped in
277277
ast.BinOp nodes. Something like "a" + "b" + "c" or "a %s" % val etc.
278278
The provided node can be any participant in the BinOp chain.
279279
280-
:param node: (ast.Str or ast.BinOp) The node to process
281-
:param stop: (ast.Str or ast.BinOp) Optional base node to stop at
280+
:param node: (ast.Constant or ast.BinOp) The node to process
281+
:param stop: (ast.Constant or ast.BinOp) Optional base node to stop at
282282
:returns: (Tuple) the root node of the expression, the string value
283283
"""
284284

@@ -300,7 +300,10 @@ def _get(node, bits, stop=None):
300300
node = node._bandit_parent
301301
if isinstance(node, ast.BinOp):
302302
_get(node, bits, stop)
303-
return (node, " ".join([x.s for x in bits if isinstance(x, ast.Str)]))
303+
return (
304+
node,
305+
" ".join([x.value for x in bits if isinstance(x, ast.Constant)]),
306+
)
304307

305308

306309
def get_called_name(node):
@@ -361,6 +364,17 @@ def parse_ini_file(f_loc):
361364
def check_ast_node(name):
362365
"Check if the given name is that of a valid AST node."
363366
try:
367+
# These ast Node types don't exist in Python 3.14, but plugins may
368+
# still check on them.
369+
if sys.version_info >= (3, 14) and name in (
370+
"Num",
371+
"Str",
372+
"Ellipsis",
373+
"NameConstant",
374+
"Bytes",
375+
):
376+
return name
377+
364378
node = getattr(ast, name)
365379
if issubclass(node, ast.AST):
366380
return name

bandit/plugins/django_sql_injection.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ def django_extra_used(context):
6868
if key in kwargs:
6969
if isinstance(kwargs[key], ast.List):
7070
for val in kwargs[key].elts:
71-
if not isinstance(val, ast.Str):
71+
if not (
72+
isinstance(val, ast.Constant)
73+
and isinstance(val.value, str)
74+
):
7275
insecure = True
7376
break
7477
else:
@@ -77,12 +80,18 @@ def django_extra_used(context):
7780
if not insecure and "select" in kwargs:
7881
if isinstance(kwargs["select"], ast.Dict):
7982
for k in kwargs["select"].keys:
80-
if not isinstance(k, ast.Str):
83+
if not (
84+
isinstance(k, ast.Constant)
85+
and isinstance(k.value, str)
86+
):
8187
insecure = True
8288
break
8389
if not insecure:
8490
for v in kwargs["select"].values:
85-
if not isinstance(v, ast.Str):
91+
if not (
92+
isinstance(v, ast.Constant)
93+
and isinstance(v.value, str)
94+
):
8695
insecure = True
8796
break
8897
else:
@@ -135,7 +144,9 @@ def django_rawsql_used(context):
135144
kwargs = keywords2dict(context.node.keywords)
136145
sql = kwargs["sql"]
137146

138-
if not isinstance(sql, ast.Str):
147+
if not (
148+
isinstance(sql, ast.Constant) and isinstance(sql.value, str)
149+
):
139150
return bandit.Issue(
140151
severity=bandit.MEDIUM,
141152
confidence=bandit.MEDIUM,

bandit/plugins/django_xss.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def evaluate_var(xss_var, parent, until, ignore_nodes=None):
9696
break
9797
to = analyser.is_assigned(node)
9898
if to:
99-
if isinstance(to, ast.Str):
99+
if isinstance(to, ast.Constant) and isinstance(to.value, str):
100100
secure = True
101101
elif isinstance(to, ast.Name):
102102
secure = evaluate_var(to, parent, to.lineno, ignore_nodes)
@@ -105,7 +105,9 @@ def evaluate_var(xss_var, parent, until, ignore_nodes=None):
105105
elif isinstance(to, (list, tuple)):
106106
num_secure = 0
107107
for some_to in to:
108-
if isinstance(some_to, ast.Str):
108+
if isinstance(some_to, ast.Constant) and isinstance(
109+
some_to.value, str
110+
):
109111
num_secure += 1
110112
elif isinstance(some_to, ast.Name):
111113
if evaluate_var(
@@ -131,7 +133,10 @@ def evaluate_call(call, parent, ignore_nodes=None):
131133
secure = False
132134
evaluate = False
133135
if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute):
134-
if isinstance(call.func.value, ast.Str) and call.func.attr == "format":
136+
if (
137+
isinstance(call.func.value, ast.Constant)
138+
and call.func.attr == "format"
139+
):
135140
evaluate = True
136141
if call.keywords:
137142
evaluate = False # TODO(??) get support for this
@@ -140,7 +145,7 @@ def evaluate_call(call, parent, ignore_nodes=None):
140145
args = list(call.args)
141146
num_secure = 0
142147
for arg in args:
143-
if isinstance(arg, ast.Str):
148+
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
144149
num_secure += 1
145150
elif isinstance(arg, ast.Name):
146151
if evaluate_var(arg, parent, call.lineno, ignore_nodes):
@@ -167,7 +172,9 @@ def evaluate_call(call, parent, ignore_nodes=None):
167172
def transform2call(var):
168173
if isinstance(var, ast.BinOp):
169174
is_mod = isinstance(var.op, ast.Mod)
170-
is_left_str = isinstance(var.left, ast.Str)
175+
is_left_str = isinstance(var.left, ast.Constant) and isinstance(
176+
var.left.value, str
177+
)
171178
if is_mod and is_left_str:
172179
new_call = ast.Call()
173180
new_call.args = []
@@ -212,7 +219,9 @@ def check_risk(node):
212219
secure = evaluate_call(xss_var, parent)
213220
elif isinstance(xss_var, ast.BinOp):
214221
is_mod = isinstance(xss_var.op, ast.Mod)
215-
is_left_str = isinstance(xss_var.left, ast.Str)
222+
is_left_str = isinstance(xss_var.left, ast.Constant) and isinstance(
223+
xss_var.left.value, str
224+
)
216225
if is_mod and is_left_str:
217226
parent = node._bandit_parent
218227
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
@@ -272,5 +281,7 @@ def django_mark_safe(context):
272281
]
273282
if context.call_function_name in affected_functions:
274283
xss = context.node.args[0]
275-
if not isinstance(xss, ast.Str):
284+
if not (
285+
isinstance(xss, ast.Constant) and isinstance(xss.value, str)
286+
):
276287
return check_risk(context.node)

bandit/plugins/general_hardcoded_password.py

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,45 +83,53 @@ def hardcoded_password_string(context):
8383
# looks for "candidate='some_string'"
8484
for targ in node._bandit_parent.targets:
8585
if isinstance(targ, ast.Name) and RE_CANDIDATES.search(targ.id):
86-
return _report(node.s)
86+
return _report(node.value)
8787
elif isinstance(targ, ast.Attribute) and RE_CANDIDATES.search(
8888
targ.attr
8989
):
90-
return _report(node.s)
90+
return _report(node.value)
9191

9292
elif isinstance(
9393
node._bandit_parent, ast.Subscript
94-
) and RE_CANDIDATES.search(node.s):
94+
) and RE_CANDIDATES.search(node.value):
9595
# Py39+: looks for "dict[candidate]='some_string'"
9696
# subscript -> index -> string
9797
assign = node._bandit_parent._bandit_parent
98-
if isinstance(assign, ast.Assign) and isinstance(
99-
assign.value, ast.Str
98+
if (
99+
isinstance(assign, ast.Assign)
100+
and isinstance(assign.value, ast.Constant)
101+
and isinstance(assign.value.value, str)
100102
):
101-
return _report(assign.value.s)
103+
return _report(assign.value.value)
102104

103105
elif isinstance(node._bandit_parent, ast.Index) and RE_CANDIDATES.search(
104-
node.s
106+
node.value
105107
):
106108
# looks for "dict[candidate]='some_string'"
107109
# assign -> subscript -> index -> string
108110
assign = node._bandit_parent._bandit_parent._bandit_parent
109-
if isinstance(assign, ast.Assign) and isinstance(
110-
assign.value, ast.Str
111+
if (
112+
isinstance(assign, ast.Assign)
113+
and isinstance(assign.value, ast.Constant)
114+
and isinstance(assign.value.value, str)
111115
):
112-
return _report(assign.value.s)
116+
return _report(assign.value.value)
113117

114118
elif isinstance(node._bandit_parent, ast.Compare):
115119
# looks for "candidate == 'some_string'"
116120
comp = node._bandit_parent
117121
if isinstance(comp.left, ast.Name):
118122
if RE_CANDIDATES.search(comp.left.id):
119-
if isinstance(comp.comparators[0], ast.Str):
120-
return _report(comp.comparators[0].s)
123+
if isinstance(
124+
comp.comparators[0], ast.Constant
125+
) and isinstance(comp.comparators[0].value, str):
126+
return _report(comp.comparators[0].value)
121127
elif isinstance(comp.left, ast.Attribute):
122128
if RE_CANDIDATES.search(comp.left.attr):
123-
if isinstance(comp.comparators[0], ast.Str):
124-
return _report(comp.comparators[0].s)
129+
if isinstance(
130+
comp.comparators[0], ast.Constant
131+
) and isinstance(comp.comparators[0].value, str):
132+
return _report(comp.comparators[0].value)
125133

126134

127135
@test.checks("Call")
@@ -176,8 +184,12 @@ def hardcoded_password_funcarg(context):
176184
"""
177185
# looks for "function(candidate='some_string')"
178186
for kw in context.node.keywords:
179-
if isinstance(kw.value, ast.Str) and RE_CANDIDATES.search(kw.arg):
180-
return _report(kw.value.s)
187+
if (
188+
isinstance(kw.value, ast.Constant)
189+
and isinstance(kw.value.value, str)
190+
and RE_CANDIDATES.search(kw.arg)
191+
):
192+
return _report(kw.value.value)
181193

182194

183195
@test.checks("FunctionDef")
@@ -246,9 +258,12 @@ def hardcoded_password_default(context):
246258
if isinstance(key, (ast.Name, ast.arg)):
247259
# Skip if the default value is None
248260
if val is None or (
249-
isinstance(val, (ast.Constant, ast.NameConstant))
250-
and val.value is None
261+
isinstance(val, ast.Constant) and val.value is None
251262
):
252263
continue
253-
if isinstance(val, ast.Str) and RE_CANDIDATES.search(key.arg):
254-
return _report(val.s)
264+
if (
265+
isinstance(val, ast.Constant)
266+
and isinstance(val.value, str)
267+
and RE_CANDIDATES.search(key.arg)
268+
):
269+
return _report(val.value)

bandit/plugins/injection_shell.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616

1717
def _evaluate_shell_call(context):
18-
no_formatting = isinstance(context.node.args[0], ast.Str)
18+
no_formatting = isinstance(
19+
context.node.args[0], ast.Constant
20+
) and isinstance(context.node.args[0].value, str)
1921

2022
if no_formatting:
2123
return bandit.LOW
@@ -83,15 +85,19 @@ def has_shell(context):
8385
for key in keywords:
8486
if key.arg == "shell":
8587
val = key.value
86-
if isinstance(val, ast.Num):
87-
result = bool(val.n)
88+
if isinstance(val, ast.Constant) and (
89+
isinstance(val.value, int)
90+
or isinstance(val.value, float)
91+
or isinstance(val.value, complex)
92+
):
93+
result = bool(val.value)
8894
elif isinstance(val, ast.List):
8995
result = bool(val.elts)
9096
elif isinstance(val, ast.Dict):
9197
result = bool(val.keys)
9298
elif isinstance(val, ast.Name) and val.id in ["False", "None"]:
9399
result = False
94-
elif isinstance(val, ast.NameConstant):
100+
elif isinstance(val, ast.Constant):
95101
result = val.value
96102
else:
97103
result = True
@@ -687,7 +693,11 @@ def start_process_with_partial_path(context, config):
687693
node = node.elts[0]
688694

689695
# make sure the param is a string literal and not a var name
690-
if isinstance(node, ast.Str) and not full_path_match.match(node.s):
696+
if (
697+
isinstance(node, ast.Constant)
698+
and isinstance(node.value, str)
699+
and not full_path_match.match(node.value)
700+
):
691701
return bandit.Issue(
692702
severity=bandit.LOW,
693703
confidence=bandit.HIGH,

0 commit comments

Comments
 (0)