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