|
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 ) |