django_xss.py 9.7 KB

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