|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2020 - 2021 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 |
|
21 import AstUtilities |
|
22 |
|
23 from Security.SecurityDefaults import SecurityDefaults |
|
24 |
|
25 # This regex starts with a windows drive letter (eg C:) |
|
26 # or one of our path delimeter characters (/, \, .) |
|
27 fullPathMatchRe = re.compile(r'^(?:[A-Za-z](?=\:)|[\\\/\.])') |
|
28 |
|
29 |
|
30 def getChecks(): |
|
31 """ |
|
32 Public method to get a dictionary with checks handled by this module. |
|
33 |
|
34 @return dictionary containing checker lists containing checker function and |
|
35 list of codes |
|
36 @rtype dict |
|
37 """ |
|
38 return { |
|
39 "Call": [ |
|
40 (checkSubprocessPopenWithShell, ("S602",)), |
|
41 (checkSubprocessPopenWithoutShell, ("S603",)), |
|
42 (checkOtherFunctionWithShell, ("S604",)), |
|
43 (checkStartProcessWithShell, ("S605",)), |
|
44 (checkStartProcessWithNoShell, ("S606",)), |
|
45 (checkStartProcessWithPartialPath, ("S607",)), |
|
46 ], |
|
47 } |
|
48 |
|
49 |
|
50 def _evaluateShellCall(context): |
|
51 """ |
|
52 Function to determine the severity of a shell call. |
|
53 |
|
54 @param context context to be inspected |
|
55 @type SecurityContext |
|
56 @return severity level (L, M or H) |
|
57 @rtype str |
|
58 """ |
|
59 noFormatting = AstUtilities.isString(context.node.args[0]) |
|
60 |
|
61 if noFormatting: |
|
62 return "L" |
|
63 else: |
|
64 return "H" |
|
65 |
|
66 |
|
67 def hasShell(context): |
|
68 """ |
|
69 Function to check, if the node of the context contains the shell keyword. |
|
70 |
|
71 @param context context to be inspected |
|
72 @type SecurityContext |
|
73 @return tuple containing a flag indicating the presence of the 'shell' |
|
74 argument and flag indicating the value of the 'shell' argument |
|
75 @rtype tuple of (bool, bool) |
|
76 """ |
|
77 keywords = context.node.keywords |
|
78 result = False |
|
79 shell = False |
|
80 if 'shell' in context.callKeywords: |
|
81 shell = True |
|
82 for key in keywords: |
|
83 if key.arg == 'shell': |
|
84 val = key.value |
|
85 if AstUtilities.isNumber(val): |
|
86 result = bool(val.n) |
|
87 elif isinstance(val, ast.List): |
|
88 result = bool(val.elts) |
|
89 elif isinstance(val, ast.Dict): |
|
90 result = bool(val.keys) |
|
91 elif isinstance(val, ast.Name) and val.id in ['False', 'None']: |
|
92 result = False |
|
93 elif AstUtilities.isNameConstant(val): |
|
94 result = val.value |
|
95 else: |
|
96 result = True |
|
97 |
|
98 return shell, result |
|
99 |
|
100 |
|
101 def checkSubprocessPopenWithShell(reportError, context, config): |
|
102 """ |
|
103 Function to check for use of popen with shell equals true. |
|
104 |
|
105 @param reportError function to be used to report errors |
|
106 @type func |
|
107 @param context security context object |
|
108 @type SecurityContext |
|
109 @param config dictionary with configuration data |
|
110 @type dict |
|
111 """ |
|
112 functionNames = ( |
|
113 config["shell_injection_subprocess"] |
|
114 if config and "shell_injection_subprocess" in config else |
|
115 SecurityDefaults["shell_injection_subprocess"] |
|
116 ) |
|
117 |
|
118 if context.callFunctionNameQual in functionNames: |
|
119 shell, shellValue = hasShell(context) |
|
120 if shell and shellValue and len(context.callArgs) > 0: |
|
121 sev = _evaluateShellCall(context) |
|
122 if sev == "L": |
|
123 reportError( |
|
124 context.getLinenoForCallArg('shell') - 1, |
|
125 context.getOffsetForCallArg('shell'), |
|
126 "S602.L", |
|
127 sev, |
|
128 "H", |
|
129 ) |
|
130 else: |
|
131 reportError( |
|
132 context.getLinenoForCallArg('shell') - 1, |
|
133 context.getOffsetForCallArg('shell'), |
|
134 "S602.H", |
|
135 sev, |
|
136 "H", |
|
137 ) |
|
138 |
|
139 |
|
140 def checkSubprocessPopenWithoutShell(reportError, context, config): |
|
141 """ |
|
142 Function to check for use of popen without shell equals true. |
|
143 |
|
144 @param reportError function to be used to report errors |
|
145 @type func |
|
146 @param context security context object |
|
147 @type SecurityContext |
|
148 @param config dictionary with configuration data |
|
149 @type dict |
|
150 """ |
|
151 functionNames = ( |
|
152 config["shell_injection_subprocess"] |
|
153 if config and "shell_injection_subprocess" in config else |
|
154 SecurityDefaults["shell_injection_subprocess"] |
|
155 ) |
|
156 |
|
157 if ( |
|
158 context.callFunctionNameQual in functionNames and |
|
159 not hasShell(context)[0] |
|
160 ): |
|
161 reportError( |
|
162 context.node.lineno - 1, |
|
163 context.node.col_offset, |
|
164 "S603", |
|
165 "L", |
|
166 "H", |
|
167 ) |
|
168 |
|
169 |
|
170 def checkOtherFunctionWithShell(reportError, context, config): |
|
171 """ |
|
172 Function to check for any function with shell equals true. |
|
173 |
|
174 @param reportError function to be used to report errors |
|
175 @type func |
|
176 @param context security context object |
|
177 @type SecurityContext |
|
178 @param config dictionary with configuration data |
|
179 @type dict |
|
180 """ |
|
181 functionNames = ( |
|
182 config["shell_injection_subprocess"] |
|
183 if config and "shell_injection_subprocess" in config else |
|
184 SecurityDefaults["shell_injection_subprocess"] |
|
185 ) |
|
186 |
|
187 if context.callFunctionNameQual not in functionNames: |
|
188 shell, shellValue = hasShell(context) |
|
189 if shell and shellValue: |
|
190 reportError( |
|
191 context.getLinenoForCallArg('shell') - 1, |
|
192 context.getOffsetForCallArg('shell'), |
|
193 "S604", |
|
194 "M", |
|
195 "L", |
|
196 ) |
|
197 |
|
198 |
|
199 def checkStartProcessWithShell(reportError, context, config): |
|
200 """ |
|
201 Function to check for starting a process with a shell. |
|
202 |
|
203 @param reportError function to be used to report errors |
|
204 @type func |
|
205 @param context security context object |
|
206 @type SecurityContext |
|
207 @param config dictionary with configuration data |
|
208 @type dict |
|
209 """ |
|
210 functionNames = ( |
|
211 config["shell_injection_shell"] |
|
212 if config and "shell_injection_shell" in config else |
|
213 SecurityDefaults["shell_injection_shell"] |
|
214 ) |
|
215 |
|
216 if ( |
|
217 context.callFunctionNameQual in functionNames and |
|
218 len(context.callArgs) > 0 |
|
219 ): |
|
220 sev = _evaluateShellCall(context) |
|
221 if sev == "L": |
|
222 reportError( |
|
223 context.node.lineno - 1, |
|
224 context.node.col_offset, |
|
225 "S605.L", |
|
226 sev, |
|
227 "H", |
|
228 ) |
|
229 else: |
|
230 reportError( |
|
231 context.node.lineno - 1, |
|
232 context.node.col_offset, |
|
233 "S605.H", |
|
234 sev, |
|
235 "H", |
|
236 ) |
|
237 |
|
238 |
|
239 def checkStartProcessWithNoShell(reportError, context, config): |
|
240 """ |
|
241 Function to check for starting a process with no shell. |
|
242 |
|
243 @param reportError function to be used to report errors |
|
244 @type func |
|
245 @param context security context object |
|
246 @type SecurityContext |
|
247 @param config dictionary with configuration data |
|
248 @type dict |
|
249 """ |
|
250 functionNames = ( |
|
251 config["shell_injection_noshell"] |
|
252 if config and "shell_injection_noshell" in config else |
|
253 SecurityDefaults["shell_injection_noshell"] |
|
254 ) |
|
255 |
|
256 if context.callFunctionNameQual in functionNames: |
|
257 reportError( |
|
258 context.node.lineno - 1, |
|
259 context.node.col_offset, |
|
260 "S606", |
|
261 "L", |
|
262 "M", |
|
263 ) |
|
264 |
|
265 |
|
266 def checkStartProcessWithPartialPath(reportError, context, config): |
|
267 """ |
|
268 Function to check for starting a process with no shell. |
|
269 |
|
270 @param reportError function to be used to report errors |
|
271 @type func |
|
272 @param context security context object |
|
273 @type SecurityContext |
|
274 @param config dictionary with configuration data |
|
275 @type dict |
|
276 """ |
|
277 functionNames = ( |
|
278 config["shell_injection_subprocess"] |
|
279 if config and "shell_injection_subprocess" in config else |
|
280 SecurityDefaults["shell_injection_subprocess"] |
|
281 ) |
|
282 |
|
283 if config and "shell_injection_shell" in config: |
|
284 functionNames += config["shell_injection_shell"] |
|
285 else: |
|
286 functionNames += SecurityDefaults["shell_injection_shell"] |
|
287 |
|
288 if config and "shell_injection_noshell" in config: |
|
289 functionNames += config["shell_injection_noshell"] |
|
290 else: |
|
291 functionNames += SecurityDefaults["shell_injection_noshell"] |
|
292 |
|
293 if ( |
|
294 len(context.callArgs) and |
|
295 context.callFunctionNameQual in functionNames |
|
296 ): |
|
297 node = context.node.args[0] |
|
298 |
|
299 # some calls take an arg list, check the first part |
|
300 if isinstance(node, ast.List): |
|
301 node = node.elts[0] |
|
302 |
|
303 # make sure the param is a string literal and not a var name |
|
304 if ( |
|
305 AstUtilities.isString(node) and |
|
306 not fullPathMatchRe.match(node.s) |
|
307 ): |
|
308 reportError( |
|
309 context.node.lineno - 1, |
|
310 context.node.col_offset, |
|
311 "S607", |
|
312 "L", |
|
313 "H", |
|
314 ) |