|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing checks for potential XSS vulnerability. |
|
8 """ |
|
9 |
|
10 # |
|
11 # This is a modified version of the one found in the bandit package. |
|
12 # |
|
13 # Original Copyright 2018 Victor Torre |
|
14 # |
|
15 # SPDX-License-Identifier: Apache-2.0 |
|
16 # |
|
17 |
|
18 import ast |
|
19 import sys |
|
20 |
|
21 PY2 = sys.version_info < (3, 0, 0) |
|
22 |
|
23 |
|
24 def getChecks(): |
|
25 """ |
|
26 Public method to get a dictionary with checks handled by this module. |
|
27 |
|
28 @return dictionary containing checker lists containing checker function and |
|
29 list of codes |
|
30 @rtype dict |
|
31 """ |
|
32 return { |
|
33 "Call": [ |
|
34 (checkDjangoXssVulnerability, ("S703",)), |
|
35 ], |
|
36 } |
|
37 |
|
38 |
|
39 def checkDjangoXssVulnerability(reportError, context, config): |
|
40 """ |
|
41 Function to check for potential XSS vulnerability. |
|
42 |
|
43 @param reportError function to be used to report errors |
|
44 @type func |
|
45 @param context security context object |
|
46 @type SecurityContext |
|
47 @param config dictionary with configuration data |
|
48 @type dict |
|
49 """ |
|
50 if context.isModuleImportedLike('django.utils.safestring'): |
|
51 affectedFunctions = [ |
|
52 'mark_safe', |
|
53 'SafeText', |
|
54 'SafeUnicode', |
|
55 'SafeString', |
|
56 'SafeBytes' |
|
57 ] |
|
58 if context.callFunctionName in affectedFunctions: |
|
59 xss = context.node.args[0] |
|
60 if not isinstance(xss, ast.Str): |
|
61 checkPotentialRisk(reportError, context.node) |
|
62 |
|
63 |
|
64 def checkPotentialRisk(reportError, node): |
|
65 """ |
|
66 Function to check a given node for a potential XSS vulnerability. |
|
67 |
|
68 @param reportError function to be used to report errors |
|
69 @type func |
|
70 @param node node to be checked |
|
71 @type ast.Call |
|
72 """ |
|
73 xssVar = node.args[0] |
|
74 |
|
75 secure = False |
|
76 |
|
77 if isinstance(xssVar, ast.Name): |
|
78 # Check if the var are secure |
|
79 parent = node._securityParent |
|
80 while not isinstance(parent, (ast.Module, ast.FunctionDef)): |
|
81 parent = parent._securityParent |
|
82 |
|
83 isParam = False |
|
84 if isinstance(parent, ast.FunctionDef): |
|
85 for name in parent.args.args: |
|
86 argName = name.id if PY2 else name.arg |
|
87 if argName == xssVar.id: |
|
88 isParam = True |
|
89 break |
|
90 |
|
91 if not isParam: |
|
92 secure = evaluateVar(xssVar, parent, node.lineno) |
|
93 elif isinstance(xssVar, ast.Call): |
|
94 parent = node._securityParent |
|
95 while not isinstance(parent, (ast.Module, ast.FunctionDef)): |
|
96 parent = parent._securityParent |
|
97 secure = evaluateCall(xssVar, parent) |
|
98 elif isinstance(xssVar, ast.BinOp): |
|
99 isMod = isinstance(xssVar.op, ast.Mod) |
|
100 isLeftStr = isinstance(xssVar.left, ast.Str) |
|
101 if isMod and isLeftStr: |
|
102 parent = node._securityParent |
|
103 while not isinstance(parent, (ast.Module, ast.FunctionDef)): |
|
104 parent = parent._securityParent |
|
105 newCall = transform2call(xssVar) |
|
106 secure = evaluateCall(newCall, parent) |
|
107 |
|
108 if not secure: |
|
109 reportError( |
|
110 node.lineno - 1, |
|
111 node.col_offset, |
|
112 "S703", |
|
113 "M", |
|
114 "H" |
|
115 ) |
|
116 |
|
117 |
|
118 # TODO: carry on from here |
|
119 class DeepAssignation(object): |
|
120 """ |
|
121 Class to perform a deep analysis of an assign. |
|
122 """ |
|
123 def __init__(self, varName, ignoreNodes=None): |
|
124 """ |
|
125 Constructor |
|
126 |
|
127 @param varName name of the variable |
|
128 @type str |
|
129 @param ignoreNodes list of nodes to ignore |
|
130 @type list of ast.AST |
|
131 """ |
|
132 self.__varName = varName |
|
133 self.__ignoreNodes = ignoreNodes |
|
134 |
|
135 def isAssignedIn(self, items): |
|
136 """ |
|
137 Public method to check, if the variable is assigned to. |
|
138 |
|
139 @param items list of nodes to check against |
|
140 @type list of ast.AST |
|
141 @return list of nodes assigned |
|
142 @rtype list of ast.AST |
|
143 """ |
|
144 assigned = [] |
|
145 for astInst in items: |
|
146 newAssigned = self.isAssigned(astInst) |
|
147 if newAssigned: |
|
148 if isinstance(newAssigned, (list, tuple)): |
|
149 assigned.extend(newAssigned) |
|
150 else: |
|
151 assigned.append(newAssigned) |
|
152 |
|
153 return assigned |
|
154 |
|
155 def isAssigned(self, node): |
|
156 """ |
|
157 Public method to check assignment against a given node. |
|
158 |
|
159 @param node node to check against |
|
160 @type ast.AST |
|
161 @return flag indicating an assignement |
|
162 @rtype bool |
|
163 """ |
|
164 assigned = False |
|
165 if self.__ignoreNodes: |
|
166 if isinstance(self.__ignoreNodes, (list, tuple, object)): |
|
167 if isinstance(node, self.__ignoreNodes): |
|
168 return assigned |
|
169 |
|
170 if isinstance(node, ast.Expr): |
|
171 assigned = self.isAssigned(node.value) |
|
172 elif isinstance(node, ast.FunctionDef): |
|
173 for name in node.args.args: |
|
174 if isinstance(name, ast.Name): |
|
175 if name.id == self.var_name.id: |
|
176 # If is param the assignations are not affected |
|
177 return assigned |
|
178 |
|
179 assigned = self.isAssignedIn(node.body) |
|
180 elif isinstance(node, ast.With): |
|
181 if PY2: |
|
182 if node.optional_vars.id == self.__varName.id: |
|
183 assigned = node |
|
184 else: |
|
185 assigned = self.isAssignedIn(node.body) |
|
186 else: |
|
187 for withitem in node.items: |
|
188 varId = getattr(withitem.optional_vars, 'id', None) |
|
189 if varId == self.__varName.id: |
|
190 assigned = node |
|
191 else: |
|
192 assigned = self.isAssignedIn(node.body) |
|
193 elif PY2 and isinstance(node, ast.TryFinally): |
|
194 assigned = [] |
|
195 assigned.extend(self.isAssignedIn(node.body)) |
|
196 assigned.extend(self.isAssignedIn(node.finalbody)) |
|
197 elif PY2 and isinstance(node, ast.TryExcept): |
|
198 assigned = [] |
|
199 assigned.extend(self.isAssignedIn(node.body)) |
|
200 assigned.extend(self.isAssignedIn(node.handlers)) |
|
201 assigned.extend(self.isAssignedIn(node.orelse)) |
|
202 elif not PY2 and isinstance(node, ast.Try): |
|
203 assigned = [] |
|
204 assigned.extend(self.isAssignedIn(node.body)) |
|
205 assigned.extend(self.isAssignedIn(node.handlers)) |
|
206 assigned.extend(self.isAssignedIn(node.orelse)) |
|
207 assigned.extend(self.isAssignedIn(node.finalbody)) |
|
208 elif isinstance(node, ast.ExceptHandler): |
|
209 assigned = [] |
|
210 assigned.extend(self.isAssignedIn(node.body)) |
|
211 elif isinstance(node, (ast.If, ast.For, ast.While)): |
|
212 assigned = [] |
|
213 assigned.extend(self.isAssignedIn(node.body)) |
|
214 assigned.extend(self.isAssignedIn(node.orelse)) |
|
215 elif isinstance(node, ast.AugAssign): |
|
216 if isinstance(node.target, ast.Name): |
|
217 if node.target.id == self.__varName.id: |
|
218 assigned = node.value |
|
219 elif isinstance(node, ast.Assign) and node.targets: |
|
220 target = node.targets[0] |
|
221 if isinstance(target, ast.Name): |
|
222 if target.id == self.__varName.id: |
|
223 assigned = node.value |
|
224 elif isinstance(target, ast.Tuple): |
|
225 pos = 0 |
|
226 for name in target.elts: |
|
227 if name.id == self.__varName.id: |
|
228 assigned = node.value.elts[pos] |
|
229 break |
|
230 pos += 1 |
|
231 |
|
232 return assigned |
|
233 |
|
234 |
|
235 def evaluateVar(xssVar, parent, until, ignoreNodes=None): |
|
236 """ |
|
237 Function to evaluate a variable node for potential XSS vulnerability. |
|
238 |
|
239 @param xssVar variable node to be checked |
|
240 @type ast.Name |
|
241 @param parent parent node |
|
242 @type ast.AST |
|
243 @param until end line number to evaluate variable against |
|
244 @type int |
|
245 @param ignoreNodes list of nodes to ignore |
|
246 @type list of ast.AST |
|
247 @return flag indicating a secure evaluation |
|
248 @rtype bool |
|
249 """ |
|
250 secure = False |
|
251 if isinstance(xssVar, ast.Name): |
|
252 if isinstance(parent, ast.FunctionDef): |
|
253 for name in parent.args.args: |
|
254 argName = name.id if PY2 else name.arg |
|
255 if argName == xssVar.id: |
|
256 return False # Params are not secure |
|
257 |
|
258 analyser = DeepAssignation(xssVar, ignoreNodes) |
|
259 for node in parent.body: |
|
260 if node.lineno >= until: |
|
261 break |
|
262 to = analyser.isAssigned(node) |
|
263 if to: |
|
264 if isinstance(to, ast.Str): |
|
265 secure = True |
|
266 elif isinstance(to, ast.Name): |
|
267 secure = evaluateVar( |
|
268 to, parent, to.lineno, ignoreNodes) |
|
269 elif isinstance(to, ast.Call): |
|
270 secure = evaluateCall(to, parent, ignoreNodes) |
|
271 elif isinstance(to, (list, tuple)): |
|
272 numSecure = 0 |
|
273 for someTo in to: |
|
274 if isinstance(someTo, ast.Str): |
|
275 numSecure += 1 |
|
276 elif isinstance(someTo, ast.Name): |
|
277 if evaluateVar(someTo, parent, |
|
278 node.lineno, ignoreNodes): |
|
279 numSecure += 1 |
|
280 else: |
|
281 break |
|
282 else: |
|
283 break |
|
284 if numSecure == len(to): |
|
285 secure = True |
|
286 else: |
|
287 secure = False |
|
288 break |
|
289 else: |
|
290 secure = False |
|
291 break |
|
292 |
|
293 return secure |
|
294 |
|
295 |
|
296 def evaluateCall(call, parent, ignoreNodes=None): |
|
297 """ |
|
298 Function to evaluate a call node for potential XSS vulnerability. |
|
299 |
|
300 @param call call node to be checked |
|
301 @type ast.Call |
|
302 @param parent parent node |
|
303 @type ast.AST |
|
304 @param ignoreNodes list of nodes to ignore |
|
305 @type list of ast.AST |
|
306 @return flag indicating a secure evaluation |
|
307 @rtype bool |
|
308 """ |
|
309 secure = False |
|
310 evaluate = False |
|
311 |
|
312 if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute): |
|
313 if isinstance(call.func.value, ast.Str) and call.func.attr == 'format': |
|
314 evaluate = True |
|
315 if call.keywords or (PY2 and call.kwargs): |
|
316 evaluate = False |
|
317 |
|
318 if evaluate: |
|
319 args = list(call.args) |
|
320 if ( |
|
321 PY2 and |
|
322 call.starargs and |
|
323 isinstance(call.starargs, (ast.List, ast.Tuple)) |
|
324 ): |
|
325 args.extend(call.starargs.elts) |
|
326 |
|
327 numSecure = 0 |
|
328 for arg in args: |
|
329 if isinstance(arg, ast.Str): |
|
330 numSecure += 1 |
|
331 elif isinstance(arg, ast.Name): |
|
332 if evaluateVar(arg, parent, call.lineno, ignoreNodes): |
|
333 numSecure += 1 |
|
334 else: |
|
335 break |
|
336 elif isinstance(arg, ast.Call): |
|
337 if evaluateCall(arg, parent, ignoreNodes): |
|
338 numSecure += 1 |
|
339 else: |
|
340 break |
|
341 elif ( |
|
342 not PY2 and |
|
343 isinstance(arg, ast.Starred) and |
|
344 isinstance(arg.value, (ast.List, ast.Tuple)) |
|
345 ): |
|
346 args.extend(arg.value.elts) |
|
347 numSecure += 1 |
|
348 else: |
|
349 break |
|
350 secure = numSecure == len(args) |
|
351 |
|
352 return secure |
|
353 |
|
354 |
|
355 def transform2call(var): |
|
356 """ |
|
357 Function to transform a variable node to a call node. |
|
358 |
|
359 @param var variable node |
|
360 @type ast.BinOp |
|
361 @return call node |
|
362 @rtype ast.Call |
|
363 """ |
|
364 if isinstance(var, ast.BinOp): |
|
365 isMod = isinstance(var.op, ast.Mod) |
|
366 isLeftStr = isinstance(var.left, ast.Str) |
|
367 if isMod and isLeftStr: |
|
368 newCall = ast.Call() |
|
369 newCall.args = [] |
|
370 newCall.args = [] |
|
371 if PY2: |
|
372 newCall.starargs = None |
|
373 newCall.keywords = None |
|
374 if PY2: |
|
375 newCall.kwargs = None |
|
376 newCall.lineno = var.lineno |
|
377 newCall.func = ast.Attribute() |
|
378 newCall.func.value = var.left |
|
379 newCall.func.attr = 'format' |
|
380 if isinstance(var.right, ast.Tuple): |
|
381 newCall.args = var.right.elts |
|
382 elif PY2 and isinstance(var.right, ast.Dict): |
|
383 newCall.kwargs = var.right |
|
384 else: |
|
385 newCall.args = [var.right] |
|
386 |
|
387 return newCall |
|
388 |
|
389 return None |