diff -r 382f89c11e27 -r 646742c260bd eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py Tue Jun 09 20:10:59 2020 +0200 @@ -0,0 +1,356 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for shell injection. +""" + +# +# This is a modified version of the one found in the bandit package. +# +# Original Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import ast +import re +import sys + +# This regex starts with a windows drive letter (eg C:) +# or one of our path delimeter characters (/, \, .) +fullPathMatchRe = re.compile(r'^(?:[A-Za-z](?=\:)|[\\\/\.])') + + +def getChecks(): + """ + Public method to get a dictionary with checks handled by this module. + + @return dictionary containing checker lists containing checker function and + list of codes + @rtype dict + """ + return { + "Call": [ + (checkSubprocessPopenWithShell, ("S602",)), + (checkSubprocessPopenWithoutShell, ("S603",)), + (checkOtherFunctionWithShell, ("S604",)), + (checkStartProcessWithShell, ("S605",)), + (checkStartProcessWithNoShell, ("S606",)), + (checkStartProcessWithPartialPath, ("S607",)), + ], + } + + +def _defaultValues(key): + """ + Function to get the default values for a given check key. + + @param key key to get default values for + @type str + @return list with default values + @rtype list of str + """ + if key == "shell_injection_subprocess": + return [ + 'subprocess.Popen', + 'subprocess.call', + 'subprocess.check_call', + 'subprocess.check_output', + 'subprocess.run' + ] + elif key == "shell_injection_shell": + return [ + 'os.system', + 'os.popen', + 'os.popen2', + 'os.popen3', + 'os.popen4', + 'popen2.popen2', + 'popen2.popen3', + 'popen2.popen4', + 'popen2.Popen3', + 'popen2.Popen4', + 'commands.getoutput', + 'commands.getstatusoutput' + ] + elif key == "shell_injection_noshell": + return [ + 'os.execl', + 'os.execle', + 'os.execlp', + 'os.execlpe', + 'os.execv', + 'os.execve', + 'os.execvp', + 'os.execvpe', + 'os.spawnl', + 'os.spawnle', + 'os.spawnlp', + 'os.spawnlpe', + 'os.spawnv', + 'os.spawnve', + 'os.spawnvp', + 'os.spawnvpe', + 'os.startfile' + ] + else: + return [] + + +def _evaluateShellCall(context): + """ + Function to determine the severity of a shell call. + + @param context context to be inspected + @type SecurityContext + @return severity level (L, M or H) + @rtype str + """ + noFormatting = isinstance(context.node.args[0], ast.Str) + + if noFormatting: + return "L" + else: + return "H" + + +def hasShell(context): + """ + Function to check, if the node of the context contains the shell keyword. + + @param context context to be inspected + @type SecurityContext + @return tuple containing a flag indicating the presence of the 'shell' + argument and flag indicating the value of the 'shell' argument + @rtype tuple of (bool, bool) + """ + keywords = context.node.keywords + result = False + shell = False + if 'shell' in context.callKeywords: + shell = True + for key in keywords: + if key.arg == 'shell': + val = key.value + if isinstance(val, ast.Num): + result = bool(val.n) + elif isinstance(val, ast.List): + result = bool(val.elts) + elif isinstance(val, ast.Dict): + result = bool(val.keys) + elif isinstance(val, ast.Name) and val.id in ['False', 'None']: + result = False + elif ( + sys.version_info[0] > 2 and + isinstance(val, ast.NameConstant) + ): + result = val.value + else: + result = True + + return shell, result + + +def checkSubprocessPopenWithShell(reportError, context, config): + """ + Function to check for use of popen with shell equals true. + + @param reportError function to be used to report errors + @type func + @param context security context object + @type SecurityContext + @param config dictionary with configuration data + @type dict + """ + if config and "shell_injection_subprocess" in config: + functionNames = config["shell_injection_subprocess"] + else: + functionNames = _defaultValues("shell_injection_subprocess") + + if context.callFunctionNameQual in functionNames: + shell, shellValue = hasShell(context) + if shell and shellValue: + if len(context.callArgs) > 0: + sev = _evaluateShellCall(context) + if sev == "L": + reportError( + context.getLinenoForCallArg('shell') - 1, + context.getOffsetForCallArg('shell'), + "S602.L", + sev, + "H", + ) + else: + reportError( + context.getLinenoForCallArg('shell') - 1, + context.getOffsetForCallArg('shell'), + "S602.H", + sev, + "H", + ) + + +def checkSubprocessPopenWithoutShell(reportError, context, config): + """ + Function to check for use of popen without shell equals true. + + @param reportError function to be used to report errors + @type func + @param context security context object + @type SecurityContext + @param config dictionary with configuration data + @type dict + """ + if config and "shell_injection_subprocess" in config: + functionNames = config["shell_injection_subprocess"] + else: + functionNames = _defaultValues("shell_injection_subprocess") + + if context.callFunctionNameQual in functionNames: + if not hasShell(context)[0]: + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S603", + "L", + "H", + ) + + +def checkOtherFunctionWithShell(reportError, context, config): + """ + Function to check for any function with shell equals true. + + @param reportError function to be used to report errors + @type func + @param context security context object + @type SecurityContext + @param config dictionary with configuration data + @type dict + """ + if config and "shell_injection_subprocess" in config: + functionNames = config["shell_injection_subprocess"] + else: + functionNames = _defaultValues("shell_injection_subprocess") + + if context.callFunctionNameQual not in functionNames: + shell, shellValue = hasShell(context) + if shell and shellValue: + reportError( + context.getLinenoForCallArg('shell') - 1, + context.getOffsetForCallArg('shell'), + "S604", + "M", + "L", + ) + + +def checkStartProcessWithShell(reportError, context, config): + """ + Function to check for starting a process with a shell. + + @param reportError function to be used to report errors + @type func + @param context security context object + @type SecurityContext + @param config dictionary with configuration data + @type dict + """ + if config and "shell_injection_shell" in config: + functionNames = config["shell_injection_shell"] + else: + functionNames = _defaultValues("shell_injection_shell") + + if context.callFunctionNameQual in functionNames: + if len(context.callArgs) > 0: + sev = _evaluateShellCall(context) + if sev == "L": + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S605.L", + sev, + "H", + ) + else: + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S605.H", + sev, + "H", + ) + + +def checkStartProcessWithNoShell(reportError, context, config): + """ + Function to check for starting a process with no shell. + + @param reportError function to be used to report errors + @type func + @param context security context object + @type SecurityContext + @param config dictionary with configuration data + @type dict + """ + if config and "shell_injection_noshell" in config: + functionNames = config["shell_injection_noshell"] + else: + functionNames = _defaultValues("shell_injection_noshell") + + if context.callFunctionNameQual in functionNames: + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S606", + "L", + "M", + ) + + +def checkStartProcessWithPartialPath(reportError, context, config): + """ + Function to check for starting a process with no shell. + + @param reportError function to be used to report errors + @type func + @param context security context object + @type SecurityContext + @param config dictionary with configuration data + @type dict + """ + if config and "shell_injection_subprocess" in config: + functionNames = config["shell_injection_subprocess"] + else: + functionNames = _defaultValues("shell_injection_subprocess") + + if config and "shell_injection_shell" in config: + functionNames += config["shell_injection_shell"] + else: + functionNames += _defaultValues("shell_injection_shell") + + if config and "shell_injection_noshell" in config: + functionNames += config["shell_injection_noshell"] + else: + functionNames += _defaultValues("shell_injection_noshell") + + if len(context.callArgs): + if context.callFunctionNameQual in functionNames: + node = context.node.args[0] + + # some calls take an arg list, check the first part + if isinstance(node, ast.List): + node = node.elts[0] + + # make sure the param is a string literal and not a var name + if isinstance(node, ast.Str) and not fullPathMatchRe.match(node.s): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S607", + "L", + "H", + )