-
-
Notifications
You must be signed in to change notification settings - Fork 584
/
django_xss.py
274 lines (242 loc) · 9.6 KB
/
django_xss.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#
# Copyright 2018 Victor Torre
#
# SPDX-License-Identifier: Apache-2.0
import ast
import bandit
from bandit.core import issue
from bandit.core import test_properties as test
class DeepAssignation:
def __init__(self, var_name, ignore_nodes=None):
self.var_name = var_name
self.ignore_nodes = ignore_nodes
def is_assigned_in(self, items):
assigned = []
for ast_inst in items:
new_assigned = self.is_assigned(ast_inst)
if new_assigned:
if isinstance(new_assigned, (list, tuple)):
assigned.extend(new_assigned)
else:
assigned.append(new_assigned)
return assigned
def is_assigned(self, node):
assigned = False
if self.ignore_nodes:
if isinstance(self.ignore_nodes, (list, tuple, object)):
if isinstance(node, self.ignore_nodes):
return assigned
if isinstance(node, ast.Expr):
assigned = self.is_assigned(node.value)
elif isinstance(node, ast.FunctionDef):
for name in node.args.args:
if isinstance(name, ast.Name):
if name.id == self.var_name.id:
# If is param the assignations are not affected
return assigned
assigned = self.is_assigned_in(node.body)
elif isinstance(node, ast.With):
for withitem in node.items:
var_id = getattr(withitem.optional_vars, "id", None)
if var_id == self.var_name.id:
assigned = node
else:
assigned = self.is_assigned_in(node.body)
elif isinstance(node, ast.Try):
assigned = []
assigned.extend(self.is_assigned_in(node.body))
assigned.extend(self.is_assigned_in(node.handlers))
assigned.extend(self.is_assigned_in(node.orelse))
assigned.extend(self.is_assigned_in(node.finalbody))
elif isinstance(node, ast.ExceptHandler):
assigned = []
assigned.extend(self.is_assigned_in(node.body))
elif isinstance(node, (ast.If, ast.For, ast.While)):
assigned = []
assigned.extend(self.is_assigned_in(node.body))
assigned.extend(self.is_assigned_in(node.orelse))
elif isinstance(node, ast.AugAssign):
if isinstance(node.target, ast.Name):
if node.target.id == self.var_name.id:
assigned = node.value
elif isinstance(node, ast.Assign) and node.targets:
target = node.targets[0]
if isinstance(target, ast.Name):
if target.id == self.var_name.id:
assigned = node.value
elif isinstance(target, ast.Tuple):
pos = 0
for name in target.elts:
if name.id == self.var_name.id:
assigned = node.value.elts[pos]
break
pos += 1
return assigned
def evaluate_var(xss_var, parent, until, ignore_nodes=None):
secure = False
if isinstance(xss_var, ast.Name):
if isinstance(parent, ast.FunctionDef):
for name in parent.args.args:
if name.arg == xss_var.id:
return False # Params are not secure
analyser = DeepAssignation(xss_var, ignore_nodes)
for node in parent.body:
if node.lineno >= until:
break
to = analyser.is_assigned(node)
if to:
if isinstance(to, ast.Str):
secure = True
elif isinstance(to, ast.Name):
secure = evaluate_var(to, parent, to.lineno, ignore_nodes)
elif isinstance(to, ast.Call):
secure = evaluate_call(to, parent, ignore_nodes)
elif isinstance(to, (list, tuple)):
num_secure = 0
for some_to in to:
if isinstance(some_to, ast.Str):
num_secure += 1
elif isinstance(some_to, ast.Name):
if evaluate_var(
some_to, parent, node.lineno, ignore_nodes
):
num_secure += 1
else:
break
else:
break
if num_secure == len(to):
secure = True
else:
secure = False
break
else:
secure = False
break
return secure
def evaluate_call(call, parent, ignore_nodes=None):
secure = False
evaluate = False
if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute):
if isinstance(call.func.value, ast.Str) and call.func.attr == "format":
evaluate = True
if call.keywords:
evaluate = False # TODO(??) get support for this
if evaluate:
args = list(call.args)
num_secure = 0
for arg in args:
if isinstance(arg, ast.Str):
num_secure += 1
elif isinstance(arg, ast.Name):
if evaluate_var(arg, parent, call.lineno, ignore_nodes):
num_secure += 1
else:
break
elif isinstance(arg, ast.Call):
if evaluate_call(arg, parent, ignore_nodes):
num_secure += 1
else:
break
elif isinstance(arg, ast.Starred) and isinstance(
arg.value, (ast.List, ast.Tuple)
):
args.extend(arg.value.elts)
num_secure += 1
else:
break
secure = num_secure == len(args)
return secure
def transform2call(var):
if isinstance(var, ast.BinOp):
is_mod = isinstance(var.op, ast.Mod)
is_left_str = isinstance(var.left, ast.Str)
if is_mod and is_left_str:
new_call = ast.Call()
new_call.args = []
new_call.args = []
new_call.keywords = None
new_call.lineno = var.lineno
new_call.func = ast.Attribute()
new_call.func.value = var.left
new_call.func.attr = "format"
if isinstance(var.right, ast.Tuple):
new_call.args = var.right.elts
else:
new_call.args = [var.right]
return new_call
def check_risk(node):
description = "Potential XSS on mark_safe function."
xss_var = node.args[0]
secure = False
if isinstance(xss_var, ast.Name):
# Check if the var are secure
parent = node._bandit_parent
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
parent = parent._bandit_parent
is_param = False
if isinstance(parent, ast.FunctionDef):
for name in parent.args.args:
if name.arg == xss_var.id:
is_param = True
break
if not is_param:
secure = evaluate_var(xss_var, parent, node.lineno)
elif isinstance(xss_var, ast.Call):
parent = node._bandit_parent
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
parent = parent._bandit_parent
secure = evaluate_call(xss_var, parent)
elif isinstance(xss_var, ast.BinOp):
is_mod = isinstance(xss_var.op, ast.Mod)
is_left_str = isinstance(xss_var.left, ast.Str)
if is_mod and is_left_str:
parent = node._bandit_parent
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
parent = parent._bandit_parent
new_call = transform2call(xss_var)
secure = evaluate_call(new_call, parent)
if not secure:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
cwe=issue.Cwe.BASIC_XSS,
text=description,
)
@test.checks("Call")
@test.test_id("B703")
def django_mark_safe(context):
"""**B703: Potential XSS on mark_safe function**
:Example:
.. code-block:: none
>> Issue: [B703:django_mark_safe] Potential XSS on mark_safe function.
Severity: Medium Confidence: High
CWE: CWE-80 (https://cwe.mitre.org/data/definitions/80.html)
Location: examples/mark_safe_insecure.py:159:4
More Info: https://bandit.readthedocs.io/en/latest/plugins/b703_django_mark_safe.html
158 str_arg = 'could be insecure'
159 safestring.mark_safe(str_arg)
.. seealso::
- https://docs.djangoproject.com/en/dev/topics/security/\
#cross-site-scripting-xss-protection
- https://docs.djangoproject.com/en/dev/ref/utils/\
#module-django.utils.safestring
- https://docs.djangoproject.com/en/dev/ref/utils/\
#django.utils.html.format_html
- https://cwe.mitre.org/data/definitions/80.html
.. versionadded:: 1.5.0
.. versionchanged:: 1.7.3
CWE information added
""" # noqa: E501
if context.is_module_imported_like("django.utils.safestring"):
affected_functions = [
"mark_safe",
"SafeText",
"SafeUnicode",
"SafeString",
"SafeBytes",
]
if context.call_function_name in affected_functions:
xss = context.node.args[0]
if not isinstance(xss, ast.Str):
return check_risk(context.node)