diff -r 4e8b98454baa -r 800c432b34c8 eric7/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py Sat May 15 18:45:04 2021 +0200 @@ -0,0 +1,314 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 - 2021 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 AstUtilities + +from Security.SecurityDefaults import SecurityDefaults + +# 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 _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 = AstUtilities.isString(context.node.args[0]) + + 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 AstUtilities.isNumber(val): + 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 AstUtilities.isNameConstant(val): + 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 + """ + functionNames = ( + config["shell_injection_subprocess"] + if config and "shell_injection_subprocess" in config else + SecurityDefaults["shell_injection_subprocess"] + ) + + if context.callFunctionNameQual in functionNames: + shell, shellValue = hasShell(context) + if shell and shellValue and 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 + """ + functionNames = ( + config["shell_injection_subprocess"] + if config and "shell_injection_subprocess" in config else + SecurityDefaults["shell_injection_subprocess"] + ) + + if ( + context.callFunctionNameQual in functionNames and + 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 + """ + functionNames = ( + config["shell_injection_subprocess"] + if config and "shell_injection_subprocess" in config else + SecurityDefaults["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 + """ + functionNames = ( + config["shell_injection_shell"] + if config and "shell_injection_shell" in config else + SecurityDefaults["shell_injection_shell"] + ) + + if ( + context.callFunctionNameQual in functionNames and + 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 + """ + functionNames = ( + config["shell_injection_noshell"] + if config and "shell_injection_noshell" in config else + SecurityDefaults["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 + """ + functionNames = ( + config["shell_injection_subprocess"] + if config and "shell_injection_subprocess" in config else + SecurityDefaults["shell_injection_subprocess"] + ) + + if config and "shell_injection_shell" in config: + functionNames += config["shell_injection_shell"] + else: + functionNames += SecurityDefaults["shell_injection_shell"] + + if config and "shell_injection_noshell" in config: + functionNames += config["shell_injection_noshell"] + else: + functionNames += SecurityDefaults["shell_injection_noshell"] + + if ( + len(context.callArgs) and + 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 ( + AstUtilities.isString(node) and + not fullPathMatchRe.match(node.s) + ): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S607", + "L", + "H", + )