eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py

changeset 7614
646742c260bd
child 7615
ca2949b1a29a
equal deleted inserted replaced
7613:382f89c11e27 7614:646742c260bd
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a check for shell injection.
8 """
9
10 #
11 # This is a modified version of the one found in the bandit package.
12 #
13 # Original Copyright 2014 Hewlett-Packard Development Company, L.P.
14 #
15 # SPDX-License-Identifier: Apache-2.0
16 #
17
18 import ast
19 import re
20 import sys
21
22 # This regex starts with a windows drive letter (eg C:)
23 # or one of our path delimeter characters (/, \, .)
24 fullPathMatchRe = re.compile(r'^(?:[A-Za-z](?=\:)|[\\\/\.])')
25
26
27 def getChecks():
28 """
29 Public method to get a dictionary with checks handled by this module.
30
31 @return dictionary containing checker lists containing checker function and
32 list of codes
33 @rtype dict
34 """
35 return {
36 "Call": [
37 (checkSubprocessPopenWithShell, ("S602",)),
38 (checkSubprocessPopenWithoutShell, ("S603",)),
39 (checkOtherFunctionWithShell, ("S604",)),
40 (checkStartProcessWithShell, ("S605",)),
41 (checkStartProcessWithNoShell, ("S606",)),
42 (checkStartProcessWithPartialPath, ("S607",)),
43 ],
44 }
45
46
47 def _defaultValues(key):
48 """
49 Function to get the default values for a given check key.
50
51 @param key key to get default values for
52 @type str
53 @return list with default values
54 @rtype list of str
55 """
56 if key == "shell_injection_subprocess":
57 return [
58 'subprocess.Popen',
59 'subprocess.call',
60 'subprocess.check_call',
61 'subprocess.check_output',
62 'subprocess.run'
63 ]
64 elif key == "shell_injection_shell":
65 return [
66 'os.system',
67 'os.popen',
68 'os.popen2',
69 'os.popen3',
70 'os.popen4',
71 'popen2.popen2',
72 'popen2.popen3',
73 'popen2.popen4',
74 'popen2.Popen3',
75 'popen2.Popen4',
76 'commands.getoutput',
77 'commands.getstatusoutput'
78 ]
79 elif key == "shell_injection_noshell":
80 return [
81 'os.execl',
82 'os.execle',
83 'os.execlp',
84 'os.execlpe',
85 'os.execv',
86 'os.execve',
87 'os.execvp',
88 'os.execvpe',
89 'os.spawnl',
90 'os.spawnle',
91 'os.spawnlp',
92 'os.spawnlpe',
93 'os.spawnv',
94 'os.spawnve',
95 'os.spawnvp',
96 'os.spawnvpe',
97 'os.startfile'
98 ]
99 else:
100 return []
101
102
103 def _evaluateShellCall(context):
104 """
105 Function to determine the severity of a shell call.
106
107 @param context context to be inspected
108 @type SecurityContext
109 @return severity level (L, M or H)
110 @rtype str
111 """
112 noFormatting = isinstance(context.node.args[0], ast.Str)
113
114 if noFormatting:
115 return "L"
116 else:
117 return "H"
118
119
120 def hasShell(context):
121 """
122 Function to check, if the node of the context contains the shell keyword.
123
124 @param context context to be inspected
125 @type SecurityContext
126 @return tuple containing a flag indicating the presence of the 'shell'
127 argument and flag indicating the value of the 'shell' argument
128 @rtype tuple of (bool, bool)
129 """
130 keywords = context.node.keywords
131 result = False
132 shell = False
133 if 'shell' in context.callKeywords:
134 shell = True
135 for key in keywords:
136 if key.arg == 'shell':
137 val = key.value
138 if isinstance(val, ast.Num):
139 result = bool(val.n)
140 elif isinstance(val, ast.List):
141 result = bool(val.elts)
142 elif isinstance(val, ast.Dict):
143 result = bool(val.keys)
144 elif isinstance(val, ast.Name) and val.id in ['False', 'None']:
145 result = False
146 elif (
147 sys.version_info[0] > 2 and
148 isinstance(val, ast.NameConstant)
149 ):
150 result = val.value
151 else:
152 result = True
153
154 return shell, result
155
156
157 def checkSubprocessPopenWithShell(reportError, context, config):
158 """
159 Function to check for use of popen with shell equals true.
160
161 @param reportError function to be used to report errors
162 @type func
163 @param context security context object
164 @type SecurityContext
165 @param config dictionary with configuration data
166 @type dict
167 """
168 if config and "shell_injection_subprocess" in config:
169 functionNames = config["shell_injection_subprocess"]
170 else:
171 functionNames = _defaultValues("shell_injection_subprocess")
172
173 if context.callFunctionNameQual in functionNames:
174 shell, shellValue = hasShell(context)
175 if shell and shellValue:
176 if len(context.callArgs) > 0:
177 sev = _evaluateShellCall(context)
178 if sev == "L":
179 reportError(
180 context.getLinenoForCallArg('shell') - 1,
181 context.getOffsetForCallArg('shell'),
182 "S602.L",
183 sev,
184 "H",
185 )
186 else:
187 reportError(
188 context.getLinenoForCallArg('shell') - 1,
189 context.getOffsetForCallArg('shell'),
190 "S602.H",
191 sev,
192 "H",
193 )
194
195
196 def checkSubprocessPopenWithoutShell(reportError, context, config):
197 """
198 Function to check for use of popen without shell equals true.
199
200 @param reportError function to be used to report errors
201 @type func
202 @param context security context object
203 @type SecurityContext
204 @param config dictionary with configuration data
205 @type dict
206 """
207 if config and "shell_injection_subprocess" in config:
208 functionNames = config["shell_injection_subprocess"]
209 else:
210 functionNames = _defaultValues("shell_injection_subprocess")
211
212 if context.callFunctionNameQual in functionNames:
213 if not hasShell(context)[0]:
214 reportError(
215 context.node.lineno - 1,
216 context.node.col_offset,
217 "S603",
218 "L",
219 "H",
220 )
221
222
223 def checkOtherFunctionWithShell(reportError, context, config):
224 """
225 Function to check for any function with shell equals true.
226
227 @param reportError function to be used to report errors
228 @type func
229 @param context security context object
230 @type SecurityContext
231 @param config dictionary with configuration data
232 @type dict
233 """
234 if config and "shell_injection_subprocess" in config:
235 functionNames = config["shell_injection_subprocess"]
236 else:
237 functionNames = _defaultValues("shell_injection_subprocess")
238
239 if context.callFunctionNameQual not in functionNames:
240 shell, shellValue = hasShell(context)
241 if shell and shellValue:
242 reportError(
243 context.getLinenoForCallArg('shell') - 1,
244 context.getOffsetForCallArg('shell'),
245 "S604",
246 "M",
247 "L",
248 )
249
250
251 def checkStartProcessWithShell(reportError, context, config):
252 """
253 Function to check for starting a process with a shell.
254
255 @param reportError function to be used to report errors
256 @type func
257 @param context security context object
258 @type SecurityContext
259 @param config dictionary with configuration data
260 @type dict
261 """
262 if config and "shell_injection_shell" in config:
263 functionNames = config["shell_injection_shell"]
264 else:
265 functionNames = _defaultValues("shell_injection_shell")
266
267 if context.callFunctionNameQual in functionNames:
268 if len(context.callArgs) > 0:
269 sev = _evaluateShellCall(context)
270 if sev == "L":
271 reportError(
272 context.node.lineno - 1,
273 context.node.col_offset,
274 "S605.L",
275 sev,
276 "H",
277 )
278 else:
279 reportError(
280 context.node.lineno - 1,
281 context.node.col_offset,
282 "S605.H",
283 sev,
284 "H",
285 )
286
287
288 def checkStartProcessWithNoShell(reportError, context, config):
289 """
290 Function to check for starting a process with no shell.
291
292 @param reportError function to be used to report errors
293 @type func
294 @param context security context object
295 @type SecurityContext
296 @param config dictionary with configuration data
297 @type dict
298 """
299 if config and "shell_injection_noshell" in config:
300 functionNames = config["shell_injection_noshell"]
301 else:
302 functionNames = _defaultValues("shell_injection_noshell")
303
304 if context.callFunctionNameQual in functionNames:
305 reportError(
306 context.node.lineno - 1,
307 context.node.col_offset,
308 "S606",
309 "L",
310 "M",
311 )
312
313
314 def checkStartProcessWithPartialPath(reportError, context, config):
315 """
316 Function to check for starting a process with no shell.
317
318 @param reportError function to be used to report errors
319 @type func
320 @param context security context object
321 @type SecurityContext
322 @param config dictionary with configuration data
323 @type dict
324 """
325 if config and "shell_injection_subprocess" in config:
326 functionNames = config["shell_injection_subprocess"]
327 else:
328 functionNames = _defaultValues("shell_injection_subprocess")
329
330 if config and "shell_injection_shell" in config:
331 functionNames += config["shell_injection_shell"]
332 else:
333 functionNames += _defaultValues("shell_injection_shell")
334
335 if config and "shell_injection_noshell" in config:
336 functionNames += config["shell_injection_noshell"]
337 else:
338 functionNames += _defaultValues("shell_injection_noshell")
339
340 if len(context.callArgs):
341 if context.callFunctionNameQual in functionNames:
342 node = context.node.args[0]
343
344 # some calls take an arg list, check the first part
345 if isinstance(node, ast.List):
346 node = node.elts[0]
347
348 # make sure the param is a string literal and not a var name
349 if isinstance(node, ast.Str) and not fullPathMatchRe.match(node.s):
350 reportError(
351 context.node.lineno - 1,
352 context.node.col_offset,
353 "S607",
354 "L",
355 "H",
356 )

eric ide

mercurial