Tue, 09 Jun 2020 20:10:59 +0200
Code Style Checker: continued to implement checker for security related issues.
--- a/eric6.e4p Mon Jun 08 20:08:27 2020 +0200 +++ b/eric6.e4p Tue Jun 09 20:10:59 2020 +0200 @@ -326,10 +326,19 @@ <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/certificateValidation.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoSqlInjection.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/exec.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/flaskDebug.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalBindAllInterfaces.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalFilePermissions.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedPassword.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedTmp.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionParamiko.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureHashlibNew.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/yamlLoad.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityDefaults.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityNodeVisitor.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/__init__.py</Source>
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Mon Jun 08 20:08:27 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Tue Jun 09 20:10:59 2020 +0200 @@ -248,9 +248,12 @@ fixable = False itm = QTreeWidgetItem( - self.__lastFileItem, - ["{0:6}".format(result["line"]), result["code"], - result["display"]]) + self.__lastFileItem, [ + "{0:6}".format(result["line"]), + result["code"], + result["display"] + ] + ) if result["code"].startswith(("W", "-", "C", "M")): itm.setIcon(1, UI.PixmapCache.getIcon("warning")) elif result["code"].startswith(("A", "N")): @@ -293,7 +296,7 @@ itm.setData(0, self.positionRole, int(result["offset"])) itm.setData(0, self.messageRole, result["display"]) itm.setData(0, self.fixableRole, fixable) - itm.setData(0, self.codeRole, result["code"]) + itm.setData(0, self.codeRole, result["code"].split(".", 1)[0]) itm.setData(0, self.ignoredRole, result["ignored"]) itm.setData(0, self.argsRole, result["args"]) @@ -448,6 +451,8 @@ "MaximumComplexity": 3, } + # TODO: add 'SecurityChecker' + self.__initCategoriesList(self.__data["EnabledCheckerCategories"]) self.excludeFilesEdit.setText(self.__data["ExcludeFiles"]) self.excludeMessagesEdit.setText(self.__data["ExcludeMessages"]) @@ -596,7 +601,7 @@ self.maxAnnotationsComplexitySpinBox.value(), } - # TODO: implement safety arguments + # TODO: add 'SecurityChecker' safetyArgs = {} self.__options = [excludeMessages, includeMessages, repeatMessages, @@ -944,6 +949,8 @@ "MaximumComplexity": self.maxAnnotationsComplexitySpinBox.value(), } + + # TODO: add 'SecurityChecker' } if data != self.__data: self.__data = data @@ -1041,6 +1048,7 @@ vm.openSourceFile(fn, lineno=lineno, pos=position + 1) editor = vm.getOpenEditor(fn) + # TODO: add other syntax errors or do syntax check once for all if code in ["E901", "E902"]: editor.toggleSyntaxError(lineno, 0, True, message, True) else: @@ -1177,6 +1185,8 @@ Preferences.Prefs.settings.value( "PEP8/MaximumAnnotationComplexity", 3))) + # TODO: add 'SecurityChecker' + self.__cleanupData() @pyqtSlot() @@ -1243,6 +1253,8 @@ Preferences.Prefs.settings.setValue( "PEP8/MaximumAnnotationComplexity", self.maxAnnotationsComplexitySpinBox.value()) + + # TODO: add 'SecurityChecker' @pyqtSlot() def on_resetDefaultButton_clicked(self): @@ -1290,6 +1302,8 @@ Preferences.Prefs.settings.setValue( "PEP8/MaximumAnnotationComplexity", 3) + # TODO: add 'SecurityChecker' + # Update UI with default values self.on_loadDefaultButton_clicked()
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/assert.py Mon Jun 08 20:08:27 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/assert.py Tue Jun 09 20:10:59 2020 +0200 @@ -43,7 +43,7 @@ @type dict """ reportError( - context.node.lineno, + context.node.lineno - 1, context.node.col_offset, "S101", "L",
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py Mon Jun 08 20:08:27 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py Tue Jun 09 20:10:59 2020 +0200 @@ -18,7 +18,7 @@ import ast import sys -PY2 = sys.version_info < (3, 0, 0) +PY2 = sys.version_info[0] == 2 def getChecks():
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/exec.py Tue Jun 09 20:10:59 2020 +0200 @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for the use of 'exec'. +""" + +# +# 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 sys + + +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 + """ + if sys.version_info[0] == 2: + return { + "Exec": [ + (checkExecUsed, ("S102",)), + ], + } + else: + return { + "Call": [ + (checkExecUsed, ("S102",)), + ], + } + + +def checkExecUsed(reportError, context, config): + """ + Function to check for the use of 'exec'. + + @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 ( + sys.version_info[0] == 2 or + context.callFunctionNameQual == 'exec' + ): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S102", + "M", + "H" + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalBindAllInterfaces.py Tue Jun 09 20:10:59 2020 +0200 @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for binding to all interfaces. +""" + +# +# 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 +# + + +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 { + "Str": [ + (checkBindAllInterfaces, ("S104",)), + ], + } + + +def checkBindAllInterfaces(reportError, context, config): + """ + Function to check for binding to all interfaces. + + @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 context.stringVal == '0.0.0.0': + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S104", + "M", + "M", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalFilePermissions.py Tue Jun 09 20:10:59 2020 +0200 @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for setting too permissive file permissions. +""" + +# +# 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 stat + + +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": [ + (checkFilePermissions, ("S102",)), + ], + } + + +def checkFilePermissions(reportError, context, config): + """ + Function to check for setting too permissive file permissions. + + @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 'chmod' in context.callFunctionName: + if context.callArgsCount == 2: + mode = context.getCallArgAtPosition(1) + + if ( + mode is not None and + isinstance(mode, int) and + (mode & stat.S_IWOTH or mode & stat.S_IXGRP) + ): + # world writable is an HIGH, group executable is a MEDIUM + if mode & stat.S_IWOTH: + severity = "H" + else: + severity = "M" + + filename = context.getCallArgAtPosition(0) + if filename is None: + filename = 'NOT PARSED' + + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S103", + severity, + "H", + oct(mode), + filename + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedPassword.py Tue Jun 09 20:10:59 2020 +0200 @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing checks for potentially hardcoded passwords. +""" + +# +# 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 + +RE_WORDS = "(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?|ken+wort|geheim)" +RE_CANDIDATES = re.compile( + '(^{0}$|_{0}_|^{0}_|_{0}$)'.format(RE_WORDS), + re.IGNORECASE +) + + +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 { + "Str": [ + (checkHardcodedPasswordAsString, ("S105",)), + ], + "Call": [ + (checkHardcodedPasswordAsFunctionArg, ("S106",)), + ], + "FunctionDef": [ + (checkHardcodedPasswordAsDefault, ("S107",)), + ], + } + + +def checkHardcodedPasswordAsString(reportError, context, config): + """ + Function to check for use of hardcoded password strings. + + @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 + """ + node = context.node + if isinstance(node._securityParent, ast.Assign): + # looks for "candidate='some_string'" + for targ in node._securityParent.targets: + if isinstance(targ, ast.Name) and RE_CANDIDATES.search(targ.id): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S105", + "L", + "M", + node.s + ) + + elif ( + isinstance(node._securityParent, ast.Index) and + RE_CANDIDATES.search(node.s) + ): + # looks for "dict[candidate]='some_string'" + # assign -> subscript -> index -> string + assign = node._securityParent._securityParent._securityParent + if ( + isinstance(assign, ast.Assign) and + isinstance(assign.value, ast.Str) + ): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S105", + "L", + "M", + assign.value.s + ) + + elif isinstance(node._securityParent, ast.Compare): + # looks for "candidate == 'some_string'" + comp = node._securityParent + if isinstance(comp.left, ast.Name): + if RE_CANDIDATES.search(comp.left.id): + if isinstance(comp.comparators[0], ast.Str): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S105", + "L", + "M", + comp.comparators[0].s + ) + + +def checkHardcodedPasswordAsFunctionArg(reportError, context, config): + """ + Function to check for use of hard-coded password function arguments. + + @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 + """ + # looks for "function(candidate='some_string')" + for kw in context.node.keywords: + if isinstance(kw.value, ast.Str) and RE_CANDIDATES.search(kw.arg): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S106", + "L", + "M", + kw.value.s + ) + + +def checkHardcodedPasswordAsDefault(reportError, context, config): + """ + Function to check for use of hard-coded password argument defaults. + + @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 + """ + # looks for "def function(candidate='some_string')" + + # this pads the list of default values with "None" if nothing is given + defs = [None] * (len(context.node.args.args) - + len(context.node.args.defaults)) + defs.extend(context.node.args.defaults) + + # go through all (param, value)s and look for candidates + for key, val in zip(context.node.args.args, defs): + isPy3Arg = True + if sys.version_info[0] > 2: + isPy3Arg = isinstance(key, ast.arg) + if isinstance(key, ast.Name) or isPy3Arg: + check = key.arg if sys.version_info[0] > 2 else key.id # Py3 + if isinstance(val, ast.Str) and RE_CANDIDATES.search(check): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S107", + "L", + "M", + val.s + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedTmp.py Tue Jun 09 20:10:59 2020 +0200 @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for insecure usage of tmp file/directory. +""" + +# +# 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 +# + + +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 { + "Str": [ + (checkHardcodedTmpDirectory, ("S108",)), + ], + } + + +def checkHardcodedTmpDirectory(reportError, context, config): + """ + Function to check for insecure usage of tmp file/directory. + + @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 "hardcoded_tmp_directories" in config: + tmpDirs = config["hardcoded_tmp_directories"] + else: + tmpDirs = ["/tmp", "/var/tmp", "/dev/shm", "~/tmp"] + + if any(context.stringVal.startswith(s) for s in tmpDirs): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S108", + "M", + "M", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionParamiko.py Tue Jun 09 20:10:59 2020 +0200 @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for shell injection within Paramiko. +""" + +# +# 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 +# + + +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": [ + (checkParamikoCalls, ("S601",)), + ], + } + + +def checkParamikoCalls(reportError, context, config): + """ + Function to check for shell injection within Paramiko. + + @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 + """ + for module in ['paramiko']: + if context.isModuleImportedLike(module): + if context.callFunctionName in ['exec_command']: + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S601", + "M", + "M", + )
--- /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", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/insecureHashlibNew.py Tue Jun 09 20:10:59 2020 +0200 @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a check for use of insecure md4, md5, or sha1 hash +functions in hashlib.new(). +""" + +# +# 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 +# + + +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": [ + (checkHashlibNew, ("S324",)), + ], + } + + +def checkHashlibNew(reportError, context, config): + """ + Function to check for use of insecure md4, md5, or sha1 hash functions + in hashlib.new(). + + @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 "insecure_hashes" in config: + insecureHashes = [h.lower() for h in config["insecure_hashes"]] + else: + insecureHashes = ['md4', 'md5', 'sha', 'sha1'] + + if isinstance(context.callFunctionNameQual, str): + qualnameList = context.callFunctionNameQual.split('.') + func = qualnameList[-1] + if 'hashlib' in qualnameList and func == 'new': + args = context.callArgs + keywords = context.callKeywords + name = args[0] if args else keywords['name'] + if ( + isinstance(name, str) and + name.lower() in insecureHashes + ): + reportError( + context.node.lineno - 1, + context.node.col_offset, + "S324", + "M", + "H", + name.upper() + )
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py Mon Jun 08 20:08:27 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py Tue Jun 09 20:10:59 2020 +0200 @@ -23,13 +23,30 @@ # assert used "S101", + # exec used + "S102", + + # bad file permissions + "S103", + + # bind to all interfaces + "S104", + + # hardcoded passwords + "S105", "S106", "S107" + + # hardcoded tmp directory + "S108", + # flask app "S201", # insecure function calls (blacklisted) "S301", "S302", "S303", "S304", "S305", "S306", "S307", "S308", "S309", "S310", "S311", "S312", "S313", "S314", "S315", "S316", "S317", "S318", - "S319", "S320", "S321", "S322", "S323", "S325", + "S319", "S320", "S321", "S322", "S323", "S325", # TODO: check S324 + # hashlib.new + "S324", # insecure imports (blacklisted) "S401", "S402", "S403", "S404", "S405", "S406", "S407", "S408", "S409", @@ -38,14 +55,17 @@ # insecure certificate usage "S501", + # YAML load + "S506", + + # Shell injection + "S601", "S602", "S603", "S604", "S605", "S606", "S607", + # Django SQL injection "S610", "S611", # Django XSS vulnerability "S703", - - # YAML load - "S506", ] def __init__(self, source, filename, select, ignore, expected, repeat, @@ -65,7 +85,7 @@ @type list of str @param repeat flag indicating to report each occurrence of a code @type bool - @param args dictionary of arguments for the miscellaneous checks + @param args dictionary of arguments for the security checks @type dict """ self.__select = tuple(select) @@ -158,12 +178,12 @@ offset = offset[1:3] else: offset = (1, 0) - self.__error(offset[0] - 1, - offset[1] or 0, - 'S999', - "H", - "H", - exc_type.__name__, exc.args[0]) + self.reportError(offset[0] - 1, + offset[1] or 0, + 'S999', + "H", + "H", + exc_type.__name__, exc.args[0]) def __generateTree(self): """
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py Mon Jun 08 20:08:27 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py Tue Jun 09 20:10:59 2020 +0200 @@ -266,14 +266,14 @@ # tend to refer to things like True and False. This prevents them from # being re-assigned in Python 3. elif ( - sys.version_info >= (3, 0, 0) and + sys.version_info[0] >= 3 and isinstance(literal, ast.NameConstant) ): literalValue = str(literal.value) # Bytes are only part of the AST in Python 3 elif ( - sys.version_info >= (3, 0, 0) and + sys.version_info[0] >= 3 and isinstance(literal, ast.Bytes) ): literalValue = literal.s
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityDefaults.py Tue Jun 09 20:10:59 2020 +0200 @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the default values for some check modules. +""" + +SecurityDefaults = { + "hardcoded_tmp_directories": ["/tmp", "/var/tmp", "/dev/shm", "~/tmp"], + "insecure_hashes": ['md4', 'md5', 'sha', 'sha1'], + "shell_injection_subprocess": [ + 'subprocess.Popen', + 'subprocess.call', + 'subprocess.check_call', + 'subprocess.check_output', + 'subprocess.run'], + "shell_injection_shell": [ + 'os.system', + 'os.popen', + 'os.popen2', + 'os.popen3', + 'os.popen4', + 'popen2.popen2', + 'popen2.popen3', + 'popen2.popen4', + 'popen2.Popen3', + 'popen2.Popen4', + 'commands.getoutput', + 'commands.getstatusoutput'], + "shell_injection_noshell": [ + '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'], +}
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py Mon Jun 08 20:08:27 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py Tue Jun 09 20:10:59 2020 +0200 @@ -15,9 +15,40 @@ # assert used "S101": QCoreApplication.translate( "Security", - "Use of assert detected. The enclosed code will be removed when" + "Use of 'assert' detected. The enclosed code will be removed when" " compiling to optimised byte code."), + # exec used + "S102": QCoreApplication.translate( + "Security", + "Use of 'exec' detected."), + + # bad file permissions + "S103": QCoreApplication.translate( + "Security", + "'chmod' setting a permissive mask {0} on file ({1})."), + + # bind to all interfaces + "S104": QCoreApplication.translate( + "Security", + "Possible binding to all interfaces."), + + # hardcoded passwords + "S105": QCoreApplication.translate( + "Security", + "Possible hardcoded password: '{0}'"), + "S106": QCoreApplication.translate( + "Security", + "Possible hardcoded password: '{0}'"), + "S107": QCoreApplication.translate( + "Security", + "Possible hardcoded password: '{0}'"), + + # hardcoded tmp directory + "S108": QCoreApplication.translate( + "Security", + "Probable insecure usage of temp file/directory."), + # flask app "S201": QCoreApplication.translate( "Security", @@ -132,6 +163,11 @@ "Use of os.tempnam() and os.tmpnam() is vulnerable to symlink" " attacks. Consider using tmpfile() instead."), + # hashlib.new + "S324": QCoreApplication.translate( + "Security", + "Use of insecure {0} hash function."), + # blacklisted imports "S401": QCoreApplication.translate( "Security", @@ -197,27 +233,65 @@ # insecure certificate usage "S501": QCoreApplication.translate( "Security", - "Requests call with verify=False disabling SSL certificate checks," + "'requests' call with verify=False disabling SSL certificate checks," " security issue."), # YAML load "S506": QCoreApplication.translate( "Security", - "Use of unsafe yaml load. Allows instantiation of arbitrary objects." - " Consider yaml.safe_load()."), + "Use of unsafe 'yaml.load()'. Allows instantiation of arbitrary" + " objects. Consider 'yaml.safe_load()'."), + + # Shell injection + "S601": QCoreApplication.translate( + "Security", + "Possible shell injection via 'Paramiko' call, check inputs are" + " properly sanitized."), + "S602.L": QCoreApplication.translate( + "Security", + "'subprocess' call with shell=True seems safe, but may be changed" + " in the future, consider rewriting without shell"), + "S602.H": QCoreApplication.translate( + "Security", + "'subprocess' call with shell=True identified, security issue."), + "S603": QCoreApplication.translate( + "Security", + "'subprocess' call - check for execution of untrusted input."), + "S604": QCoreApplication.translate( + "Security", + "Function call with shell=True parameter identified, possible" + " security issue."), + "S605.L": QCoreApplication.translate( + "Security", + "Starting a process with a shell: Seems safe, but may be changed in" + " the future, consider rewriting without shell"), + "S605.H": QCoreApplication.translate( + "Security", + "Starting a process with a shell, possible injection detected," + " security issue."), + "S606": QCoreApplication.translate( + "Security", + "Starting a process without a shell."), + "S607": QCoreApplication.translate( + "Security", + "Starting a process with a partial executable path."), # Django SQL injection "S610": QCoreApplication.translate( "Security", - "Use of extra potential SQL attack vector."), + "Use of 'extra()' opens a potential SQL attack vector."), "S611": QCoreApplication.translate( "Security", - "Use of RawSQL potential SQL attack vector."), + "Use of 'RawSQL()' opens a potential SQL attack vector."), # Django XSS vulnerability "S703": QCoreApplication.translate( "Security", - "Potential XSS on mark_safe() function."), + "Potential XSS on 'mark_safe()' function."), + + "S999": QCoreApplication.translate( + "Security", + "{0}: {1}"), ## "S": QCoreApplication.translate( ## "Security", @@ -225,6 +299,11 @@ } _securityMessagesSampleArgs = { + "S103": ["0o777", "testfile.txt"], + "S105": ["password"], + "S106": ["password"], + "S107": ["password"], + "S304": ["Crypto.Cipher.DES"], "S305": ["cryptography.hazmat.primitives.ciphers.modes.ECB"], "S313": ["xml.etree.cElementTree.parse"], @@ -236,6 +315,8 @@ "S319": ["xml.dom.pulldom.parse"], "S320": ["lxml.etree.parse"], + "S324": ["MD5"], + "S403": ["pickle"], "S404": ["subprocess"], "S405": ["xml.etree.ElementTree"], @@ -247,4 +328,6 @@ "S411": ["xmlrpclib"], "S412": ["wsgiref.handlers.CGIHandler"], "S413": ["Crypto.Cipher"], + + "S999": ["SyntaxError", "Invalid Syntax"], }